Prototype Pattern
1. What the prototype pattern is
The Prototype pattern builds a new object by cloning an existing one instead of calling a constructor. The existing instance is the prototype; new instances are produced by asking the prototype for a copy of itself. The class doing the cloning never has to know the concrete type of what it’s copying.
That’s the whole pattern. The Gang of Four put it in the creational bucket because, like the others in the series, it’s about who decides which concrete type gets instantiated. Here the answer is: an existing instance does, by being the template.
In Java specifically, “Prototype” sounds simpler than it is. The language ships a built-in cloning mechanism (Object.clone() and the Cloneable interface) that looks like an off-the-shelf implementation of the pattern, and isn’t. Most of this post is about why that’s broken and what to do instead.
2. Why clone instead of construct?
Most of the time, new Foo(...) is the right answer and there’s no reason to involve a prototype. The pattern earns its place in two specific situations, and they share a common shape: you already have an instance whose state is hard to reproduce from scratch.
3. Writing one in Java
Java has a built-in cloning mechanism, and it’s the textbook way to implement Prototype. It’s also broken enough that Joshua Bloch dedicates an entire Effective Java item to telling you not to use it. This section walks both: the built-in version first, with its sharp edges named, then the alternative every modern Java codebase actually uses.
3.1. Cloneable and Object.clone()
Every class in Java inherits clone() from Object. It’s protected, so you can’t call it from outside the class hierarchy until you override it. It also throws a checked CloneNotSupportedException unless your class implements the marker interface Cloneable — which just flips a switch inside Object.clone() that decides whether to throw.
A class that wants to participate in the built-in cloning protocol typically looks like this:
public final class Shape implements Cloneable {
private String name;
private int[] points;
public Shape(String name, int[] points) {
this.name = name;
this.points = points;
}
@Override
public Shape clone() {
try {
Shape copy = (Shape) super.clone();
copy.points = points.clone(); // deep copy the mutable array
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // can't happen, we implement Cloneable
}
}
}A few things are already strange before we even get to the interesting problems:
super.clone()doesn’t go through a constructor. It’s an extralinguistic shortcut that field-copies the object directly, bypassing whatever invariants your constructor enforces.- The cast
(Shape) super.clone()is unavoidable becauseObject.clone()returnsObject. Covariant return types let your override declareShapeas the return type, which helps callers, but the cast is still there. CloneNotSupportedExceptionis checked, so you have to wrap the call intry/catcheven though, having implementedCloneable, the throw is unreachable. Everyclone()method in every codebase ends with the samethrow new AssertionError(e)boilerplate.- The
pointsarray has to be cloned explicitly. The default shallow copy fromsuper.clone()would share the array reference between original and copy — mutate one, mutate both. Forgetting this line is one of the most common bugs in hand-writtenclone()methods and the compiler can’t help you.
Now the deeper issues. Bloch’s Effective Java Item 13 is titled “Override clone judiciously,” and the substance of it is closer to don’t:
Cloneableis a marker interface that changes the behaviour of aprotectedmethod on a superclass. Java has no other example of this pattern. Implementing an interface normally means a class commits to a contract; here it meansObject.clone()decides not to throw. The mechanism is unique, undocumented in the language spec in the way you’d expect, and easy to get wrong.clone()andfinalfields don’t mix. Sinceclone()doesn’t run a constructor, you can’t reassignfinalfields inside it. Any mutable field that needs to be deep-copied must therefore be non-final, even if you’d otherwise want itfinalfor thread safety. The pattern actively pushes you away from immutability.- The contract is too loose to be useful for subclasses. “Returns a copy of this object” is the entire spec. Whether
clone()does shallow or deep copy, whether it calls subclass constructors, whether subclasses must override it — none of that is enforced. A subclass of aCloneableclass that adds a mutable field must remember to overrideclone()and deep-copy that field, or it silently breaks. - The pattern doesn’t compose well with inheritance. If a parent class uses
clone(), every subclass has to participate, and they all have to agree on the same shallow-vs-deep semantics. This is a separate concern from regular inheritance and Java has no language feature to enforce it.
Cloneable works, but it’s a foot-gun shaped like an off-the-shelf solution. Bloch’s recommendation, and the consensus in modern Java, is to implement Prototype with one of the alternatives in §3.2 instead.
3.2. Copy constructors and copy factories
The recommended way to implement Prototype in Java is a copy constructor — a constructor that takes an instance of the same type and copies its state into a new one:
public final class Shape {
private final String name;
private final int[] points;
public Shape(String name, int[] points) {
this.name = name;
this.points = points.clone();
}
public Shape(Shape other) { // the copy constructor
this.name = other.name;
this.points = other.points.clone();
}
}This solves every problem Cloneable introduced. No checked exception, no marker interface, no cast. Fields stay final because the copy goes through a real constructor that assigns them. And the deep-copy of points is right there in the constructor body, so there’s no shallow-by-default behaviour to forget about.
A copy factory is the same idea exposed as a static method, in the same spirit as the static factory method from the Factory post:
public static Shape copyOf(Shape other) {
return new Shape(other);
}The factory variant gets you a descriptive name (Shape.copyOf(s) reads better than new Shape(s) at the call site) and the option of returning a subtype later without breaking callers. The Java collections framework leans on both: ArrayList(Collection) is a copy constructor, List.copyOf(Collection) is a copy factory.
Both versions also generalize cleanly to the polymorphic case, where the call site holds an abstract type and asks it to copy itself without knowing the concrete class. Define copy() as part of the type’s contract and every concrete implementation clones itself in whatever way it knows how:
public interface Shape {
Shape copy();
// ...other methods
}
public final class Circle implements Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public Shape copy() {
return new Circle(this.radius);
}
}This is the version that matches the GoF book’s original picture of Prototype: an abstract type whose contract includes self-copying, with each concrete subclass deciding how its own state gets carried over. The caller holds a Shape and asks it to copy itself; the dispatch is polymorphic, and no one outside Circle ever names Circle directly. None of the Cloneable machinery is involved, and Shape doesn’t even need to be a class — an interface with a single copy() method is enough to express the pattern.
4. Deep vs shallow copy
The hardest part of implementing Prototype isn’t picking a mechanism. It’s deciding, field by field, whether the copy needs to own its own version of a nested object or can safely share it with the original. A shallow copy duplicates the outer object but leaves every reference field pointing at the same target. A deep copy clones the nested objects too, recursively. The two are almost always indistinguishable until something mutates a shared reference and you suddenly have two objects, in lockstep.
The §3.2 copy constructor was careful about this:
public Shape(Shape other) {
this.name = other.name;
this.points = other.points.clone(); // <- explicit deep copy
}Drop the .clone() and the bug shows up the first time anyone mutates the array:
Shape a = new Shape("triangle", new int[]{0, 0, 1, 1, 2, 0});
Shape b = new Shape(a); // shallow copy
b.getPoints()[0] = 99; // mutating b
System.out.println(a.getPoints()[0]); // prints 99 — a was mutated tooa and b are different Shape objects, but they share the same int[]. Mutating one mutates the other. This is the entire reason cloning is tricky in any language with references, and in Java it’s load-bearing because Object.clone() is shallow by default and the compiler won’t tell you you forgot.
Which fields need explicit deep-copying and which don’t comes down to whether the field’s value can change after construction:
| Field type | Needs deep copy? | Why |
|---|---|---|
Primitives (int, long, boolean, …) | No | Copied by value at assignment. |
Immutable references (String, LocalDate, records, enums) | No | Sharing is safe — the target can’t change. |
Mutable collections (List, Map, Set) | Yes | Sharing means a add/put/remove on one is visible from the other. |
| Arrays of any element type | Yes | Arrays are always mutable. arr.clone() for primitives; element-by-element for object arrays whose elements are also mutable. |
| Custom mutable classes | Yes, recursively | Each layer needs its own copy step. |
The recursive case is where things get genuinely awkward. Take a Document containing a List<Section> where each Section contains a List<Paragraph>: the copy constructor for Document has to copy the list and copy each Section, which means Section needs its own copy constructor, which copies its list and each Paragraph, and so on. No language-level shortcut. Every mutable layer in the nesting adds a layer of copying.
This is the practical reason most real-world Prototype implementations end up favouring immutable inner types. If Section and Paragraph are immutable (records work well here), the outer Document only has to copy the top-level list — the elements can be shared safely. Pushing immutability down the object graph collapses the deep-copy problem to a single layer.
5. When not to use it
Prototype is the least-used pattern in this series, and not by accident. Modern Java has eaten most of its use cases through immutability features that the GoF book predates by decades. A few smells to watch for:
- The object is immutable. Re-stating §2 because it’s the most common over-application: if the type can’t change after construction, “cloning” it is just sharing the reference, and there’s no pattern to apply.
String,LocalDate, records, and any class with all-finalfields and no mutable nested state are out of scope by definition. - Records cover the “tweak one field” case. A common reason to reach for Prototype is “I have a configured template; give me a copy with one field changed.” If the template is a record, that’s a one-liner:
new Config(template.url(), "POST", template.timeout(), template.retries()). With Java 21+ deconstruction patterns, the syntax is even shorter. No clone protocol, no copy constructor, no pattern. - Construction wasn’t actually expensive. The original GoF motivation was object construction being slow enough that copying an existing instance was a measurable win. In ordinary application code on modern JVMs, allocation is cheap and constructors run in nanoseconds. If you’re reaching for Prototype to “save the cost of
new,” measure first — you almost certainly don’t need to. - There’s only one concrete type and no polymorphism. A copy constructor on a single class is a copy constructor, not the Prototype pattern. The pattern earns its name when the call site holds an abstract type and asks it to copy itself. Without that polymorphism, you’ve just written a useful constructor; don’t dress it up as architecture.
- A Builder with a
from(existing)method is clearer. When the use case is “start from this configured object, tweak two fields, build a new one,” the Builder pattern handles it more explicitly.HttpRequest.builder(template).timeout(...).build()reads better thantemplate.copy()followed by setter calls, and the Builder gives you a place to enforce invariants on the new instance.
The honest rule of thumb: Prototype earns its place when the object is mutable, construction is either genuinely expensive or genuinely polymorphic, and there are at least two concrete types whose copies need to dispatch through a common interface. Hit all three and the pattern is buying something. Miss any one, and a copy constructor, a record, or no copy at all does the job with less ceremony.