Cloning existing objects instead of building from scratch — and the shallow-vs-deep-copy bug that catches almost everyone the first time.
Published September 22, 2026
Some objects are expensive to build from scratch — heavy initialization, an expensive DB lookup to populate default state, or a complex object graph assembled step by step. Prototype sidesteps rebuilding all of that by cloning an existing, already-configured instance.
interface Shape extends Cloneable {
Shape clone();
}
class Circle implements Shape {
private int radius;
private String color;
Circle(int radius, String color) {
this.radius = radius;
this.color = color;
}
public Circle clone() {
return new Circle(this.radius, this.color); // manual copy, not Object.clone()
}
}
Circle template = new Circle(10, "red");
Circle copy1 = template.clone();
copy1.setColor("blue"); // independent — doesn't affect template
A naive clone only copies the object's own fields, including references — it does not clone the objects those references point to. This is fine for primitive/immutable fields, but dangerous for mutable, nested ones.
class Order implements Cloneable {
private List<String> items;
// SHALLOW clone — the bug
public Order clone() {
Order copy = new Order();
copy.items = this.items; // same list reference!
return copy;
}
}
Order original = new Order();
original.items = new ArrayList<>(List.of("book"));
Order copy = original.clone();
copy.items.add("pen"); // mutates the SAME list original.items points to
// original.items now also contains "pen" — not what "clone" implies
The fix is a deep copy — recursively cloning mutable nested objects, not just copying the outer object's references:
public Order clone() {
Order copy = new Order();
copy.items = new ArrayList<>(this.items); // new list, same elements — safe if elements are immutable
return copy;
}
If items held mutable objects themselves (not Strings), each element would need its own .clone() too — deep copying is recursive by nature, and how deep it needs to go depends entirely on which fields are actually mutable.
Prototype is worth it when object creation is genuinely expensive (a costly initialization step, or an object with dozens of pre-configured fields you'd otherwise have to re-specify every time) and you have a small number of "template" configurations that get cloned and lightly customized repeatedly — a common example is a graphics/game engine cloning pre-configured entity templates rather than re-running full initialization for every spawned instance.
When a plain copy constructor suffices — most everyday cases — prefer it; it's simpler, doesn't require implementing Cloneable (whose contract is famously awkward — Object.clone() is protected, doesn't call constructors, and its shallow-by-default behavior is exactly the bug shown above), and makes the deep-vs-shallow decision explicit and visible at the call site rather than hidden inside an overridden clone().
// Copy constructor — the more idiomatic modern alternative
class Order {
private List<String> items;
Order(Order source) { // explicit, visible copying — no Cloneable pitfalls
this.items = new ArrayList<>(source.items);
}
}
Cloneable and Object.clone() are awkward: Cloneable has no methods, clone() bypasses constructors, it's shallow by default, it doesn't work with final fields that need new values, and it throws a checked exception. Most modern Java code implements Prototype with copy constructors or copy methods instead:
public final class Order {
private final String customerId;
private final List<OrderLine> lines;
private final Address shippingAddress;
private final Map<String, String> tags;
public Order(String customerId, List<OrderLine> lines, Address shippingAddress, Map<String, String> tags) {
this.customerId = customerId;
this.lines = new ArrayList<>(lines);
this.shippingAddress = shippingAddress;
this.tags = new HashMap<>(tags);
}
/** Copy constructor: every mutable part is copied deeply enough to be independent. */
public Order(Order other) {
this.customerId = other.customerId; // immutable String: share
this.lines = other.lines.stream().map(OrderLine::new).collect(Collectors.toCollection(ArrayList::new));
this.shippingAddress = other.shippingAddress; // immutable record: share
this.tags = new HashMap<>(other.tags); // strings inside are immutable
}
/** Named copy-with-change methods read better than copy-then-mutate. */
public Order withShippingAddress(Address newAddress) {
return new Order(customerId, lines, newAddress, tags);
}
}
The rule for deciding how deep to copy: share immutable objects (strings, records with immutable fields, LocalDate), and copy mutable ones (lists, maps, mutable entities). If every component is immutable, a copy is just a new container, which is one of the many reasons to prefer immutable designs.
Java records make this easier still: they're shallowly immutable, so "prototype + change" is simply constructing a new record from the old one's components.
Prototype shines when objects are expensive or complicated to configure, and many variants start from the same few presets. Keep configured prototypes in a registry, and hand out copies:
public final class DocumentTemplates {
private final Map<String, Document> prototypes = new ConcurrentHashMap<>();
public void register(String key, Document configured) {
prototypes.put(key, new Document(configured)); // store a private copy
}
public Document create(String key) {
Document proto = prototypes.get(key);
if (proto == null) throw new IllegalArgumentException("Unknown template: " + key);
return new Document(proto); // callers always get an independent copy
}
}
templates.register("invoice-gst", invoiceWithGstHeaderFootersAndStyles);
Document inv = templates.create("invoice-gst"); // pre-styled, ready to fill in
Two copies protect the registry. One is on the way in, so the caller can't later mutate the stored prototype. The other is on the way out, so one user's edits don't leak into everyone else's documents. Forgetting either creates bugs where "every new document suddenly has yesterday's customer's name".
Real uses include:
prototype bean scope, which is named after this pattern: each injection or lookup creates a fresh instance from the bean definition.A deep-copy bug is invisible until two copies interfere in production, so test it directly:
@Test void copyIsIndependentOfOriginal() {
Order original = sampleOrderWithTwoLines();
Order copy = new Order(original);
copy.lines().get(0).setQuantity(99);
copy.tags().put("priority", "high");
assertEquals(1, original.lines().get(0).quantity());
assertFalse(original.tags().containsKey("priority"));
}
Mutate every mutable component of the copy, and assert that the original is unchanged. When someone later adds a new mutable field and forgets to copy it, this style of test catches it, especially if it's written generically, or kept next to the copy constructor.
Q: Why is Object.clone() considered a design mistake by many, including its own creators?
A: It's shallow by default (silently sharing mutable state unless every subclass remembers to override correctly), it bypasses constructors entirely (so invariants enforced in a constructor aren't re-checked), and Cloneable is a marker interface with no actual clone method on it — calling clone() on a non-Cloneable object throws CloneNotSupportedException at runtime, not compile time.
Q: Is Prototype the same as the Builder pattern? A: No — Builder constructs a new object step by step from scratch (see Builder Pattern); Prototype starts from an existing fully-formed object and copies it. They can combine (clone a prototype, then use builder-style setters to adjust the copy) but solve different problems.
Q: How would you implement Prototype using serialization instead of clone()? A: Serialize the object to bytes, then deserialize into a new instance — this produces a guaranteed deep copy automatically (every reachable object in the graph gets recreated), at the cost of serialization overhead and requiring every field's type to be serializable. A reasonable fallback when the object graph is deep and hand-writing recursive clones would be error-prone.
Q: Why prefer copy constructors over clone() in Java?
A: Copy constructors are ordinary, explicit code. They work with final fields, can't forget to call a constructor, throw no checked exceptions, and make the depth of each copy a visible decision. clone() is shallow by default, relies on the marker interface Cloneable, and is easy to get subtly wrong.
Q: How do you decide what to deep-copy?
A: Copy anything mutable that the copy could change, or that could change underneath the copy: collections, arrays, mutable entities. Immutable values (strings, records of immutable fields, java.time types) can be shared safely.
Q: How does a prototype registry differ from a factory? A: A factory builds new objects with construction logic. A prototype registry stores fully configured example objects, and produces new ones by copying them. It's useful when configuration is data-driven, or expensive, and variants are defined at runtime rather than in code.