02-creational-patterns

Prototype Pattern: Cloning Instead of Constructing

Creating new objects by copying existing ones instead of building from scratch — deep vs shallow cloning, why Java's Cloneable is broken, copy constructors, and the prototype registry.

August 11, 2026
lldcreationalprototypecloningdeep-copyshallow-copycopy-constructor

Prototype Pattern

Prototype creates new objects by copying an existing, fully-configured instance — a "prototype" — instead of constructing from scratch. It's the right tool when construction is expensive (database round-trips, file I/O, complex computed state) or when you need many objects that are nearly identical, differing only in a small number of fields set after the copy.


1. Intent

Some objects are expensive to build not because of complex control flow but because construction does real work — hits a database, parses a large file, runs an expensive computation to derive internal state. If you need several objects that all start from that same expensive baseline and then vary only slightly, paying the full construction cost for every one of them is wasteful. Prototype's intent is to pay that cost exactly once, then produce further instances by copying, which is typically far cheaper than re-running the original construction logic.


2. The Naive Approach (Violation)

java
// VIOLATION: every GameCharacter is built by re-running expensive setup —
// loading a 3D mesh from disk and computing derived stats — even when
// the caller only wants a copy with a different name.
class GameCharacter {
    private final String name;
    private final Mesh mesh;               // loaded from a file — slow
    private final Map<String, Integer> baseStats; // computed from a stat table — slow
 
    GameCharacter(String name, String meshFile) {
        this.name = name;
        this.mesh = MeshLoader.loadFromDisk(meshFile);       // expensive I/O
        this.baseStats = StatCalculator.computeBaseline();    // expensive computation
    }
}
 
// Spawning 50 orcs means re-loading the mesh and recomputing stats 50 times,
// even though every orc's mesh and base stats are IDENTICAL.
List<GameCharacter> orcs = new ArrayList<>();
for (int i = 0; i < 50; i++) {
    orcs.add(new GameCharacter("Orc-" + i, "orc.mesh")); // 50x redundant disk I/O
}

3. Structure


4. Shallow vs. Deep Copy

This is the crux of implementing Prototype correctly, and the source of most of its bugs.

java
// VIOLATION: shallow copy — mutable fields are shared by reference,
// not actually duplicated.
class GameCharacter implements Cloneable {
    String name;
    List<String> inventory; // mutable, shared reference after clone()
 
    @Override
    public GameCharacter clone() {
        try {
            return (GameCharacter) super.clone(); // Object.clone() = SHALLOW copy
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}
 
GameCharacter orcTemplate = new GameCharacter();
orcTemplate.inventory = new ArrayList<>(List.of("axe"));
 
GameCharacter orc1 = orcTemplate.clone();
orc1.inventory.add("shield"); // mutates the SAME list orcTemplate.inventory references!
 
System.out.println(orcTemplate.inventory); // ["axe", "shield"] — template corrupted!

Object.clone() performs a field-by-field copy: primitives and immutable references (like String) copy safely, but any mutable object reference — a List, another custom object, an array — is copied as the same reference, not a new object. Two "independent" clones end up silently sharing mutable sub-objects.

java
// FIXED: deep copy — mutable fields are independently duplicated
class GameCharacter implements Cloneable {
    String name;
    List<String> inventory;
 
    @Override
    public GameCharacter clone() {
        try {
            GameCharacter copy = (GameCharacter) super.clone(); // shallow copy first...
            copy.inventory = new ArrayList<>(this.inventory);   // ...then deep-copy mutable fields
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}
 
GameCharacter orc1 = orcTemplate.clone();
orc1.inventory.add("shield"); // only orc1's OWN list is mutated now
 
System.out.println(orcTemplate.inventory); // ["axe"] — template is safe
⚠️

Deep copy isn't "always copy everything, recursively, forever" — it means copying everything that's mutable and not safely shareable. Immutable fields (String, boxed primitives, another immutable value object) are safe to share by reference even in a "deep" copy; recursively cloning them is wasted work. Deciding the boundary of what actually needs deep-copying is the real design decision, not a mechanical rule.


5. Why Cloneable Is a Broken Contract in Java

Java's built-in cloning support has well-documented design problems that make most experienced teams avoid it:

java
class Broken {
    // Cloneable is a MARKER interface — it declares NO methods at all.
    // It doesn't require or even declare clone(); it only changes what
    // Object.clone() does at runtime (throws if the marker is absent).
}
 
Broken b = new Broken();
b.clone(); // COMPILE ERROR — clone() is protected on Object and Broken didn't override it

The specific problems, worth knowing individually:

  1. Cloneable declares no clone() method — implementing it doesn't give you a public clone(); you still have to override Object.clone() yourself and widen its visibility, which is easy to get wrong or forget.
  2. Object.clone() is protected and throws a checked CloneNotSupportedException — awkward for an operation that, once you've implemented it, can never actually fail.
  3. super.clone() performs a shallow copy — every class in an inheritance chain with mutable fields must remember to deep-copy them, and a subclass author can easily forget a field added in a parent class.
  4. No compiler enforcement — nothing stops a class from implementing Cloneable without correctly overriding clone(), or from forgetting Cloneable and getting a runtime CloneNotSupportedException instead of a compile error.

Effective Java (Item 13) recommends avoiding Cloneable/clone() entirely in new code and using a copy constructor or static copy factory instead.


6. Copy Constructor / Copy Factory (The Preferred Java Idiom)

java
// PREFERRED: an ordinary constructor that takes another instance of the
// same type and copies its state — no Cloneable, no checked exception,
// full control and visibility of what gets deep-copied.
class GameCharacter {
    private final String name;
    private final Mesh mesh;              // assume Mesh is immutable/shareable
    private final List<String> inventory; // mutable — must be deep-copied
 
    GameCharacter(String name, Mesh mesh, List<String> inventory) {
        this.name = name;
        this.mesh = mesh;
        this.inventory = new ArrayList<>(inventory);
    }
 
    // Copy constructor — explicit, type-safe, no exceptions to catch
    GameCharacter(GameCharacter source) {
        this.name = source.name;
        this.mesh = source.mesh;                          // safe to share — immutable
        this.inventory = new ArrayList<>(source.inventory); // deep copy — mutable
    }
 
    GameCharacter withName(String newName) {
        GameCharacter copy = new GameCharacter(this);
        return new GameCharacter(newName, copy.mesh, copy.inventory);
    }
}
 
// Usage — no checked exception, no cast, no marker interface
GameCharacter orcTemplate = new GameCharacter("Orc Template", orcMesh, List.of("axe"));
GameCharacter orc1 = new GameCharacter(orcTemplate);
Cloneable / clone()Copy constructor / static copy factory
Requires implementing a marker interfaceYesNo
Checked exception to handleYes (CloneNotSupportedException)No
Compiler enforces correct overrideNoYes — it's just a normal constructor
Works cleanly with final fieldsNo — clone() bypasses constructors, awkward with finalYes — a natural fit
Industry recommendationAvoid (Effective Java Item 13)Preferred

7. Prototype Registry

When there are several named "template" prototypes to clone from, a small registry keeps client code from needing to know how each template is originally constructed:

java
class CharacterPrototypeRegistry {
    private final Map<String, GameCharacter> prototypes = new HashMap<>();
 
    void register(String key, GameCharacter prototype) {
        prototypes.put(key, prototype);
    }
 
    GameCharacter create(String key) {
        GameCharacter prototype = prototypes.get(key);
        if (prototype == null) {
            throw new IllegalArgumentException("No prototype registered for: " + key);
        }
        return new GameCharacter(prototype); // clone via copy constructor
    }
}
 
// Setup once, at startup — pays the expensive construction cost exactly once per template:
CharacterPrototypeRegistry registry = new CharacterPrototypeRegistry();
registry.register("orc", new GameCharacter("Orc Template", MeshLoader.loadFromDisk("orc.mesh"), List.of("axe")));
registry.register("elf", new GameCharacter("Elf Template", MeshLoader.loadFromDisk("elf.mesh"), List.of("bow")));
 
// Spawning is now cheap — clone + rename, no disk I/O per spawn:
GameCharacter orc7 = registry.create("orc");

This is effectively a Factory Method that clones instead of constructing — client code asks the registry for "an orc" without knowing or caring whether that means running new GameCharacter(...) from scratch or copying a cached template.


8. Serialization-Based Deep Cloning

For object graphs too complex to hand-write field-by-field deep copying for (deeply nested structures, graphs with cycles), a serialize-then-deserialize round trip is a blunt but reliable deep-clone technique:

java
// Works for any Serializable graph — the round trip forces every reachable
// mutable object to be freshly reconstructed, guaranteeing no shared references.
static <T extends Serializable> T deepCopyViaSerialization(T original) {
    try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
         ObjectOutputStream oos = new ObjectOutputStream(bos)) {
        oos.writeObject(original);
        try (ObjectInputStream ois =
                 new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
            @SuppressWarnings("unchecked")
            T copy = (T) ois.readObject();
            return copy;
        }
    } catch (IOException | ClassNotFoundException e) {
        throw new RuntimeException("Deep copy failed", e);
    }
}
🚨

This technique is significantly slower than a hand-written copy constructor (full serialization round trip vs. direct field copies) and requires every reachable type in the object graph to implement Serializable, including transitively. Treat it as a pragmatic fallback for genuinely complex graphs, not a default — a hand-written copy constructor is faster and gives explicit, reviewable control over exactly what gets copied.


9. When to Use Prototype vs. Factory

Use Prototype whenPrefer Factory when
Construction is genuinely expensive (I/O, heavy computation) and you need several near-identical instancesConstruction is cheap — cloning buys nothing over a fresh new
Instances differ from a baseline by only a few fields, set after copyingInstances vary significantly from each other — there's no useful shared "baseline" to copy
The concrete type to produce isn't known until runtime, but an existing instance of the right type is available to copyThe concrete type is known by name/config, and building fresh from that config is straightforward
You want to avoid re-deriving computed/loaded state that a template object already holdsEach object's state needs to be independently and correctly derived every time — copying a stale template would be a correctness bug, not an optimization

10. Real-World / Production Examples

  • Object.clone() usage in the JDK itselfArrayList.clone(), HashMap.clone() (shallow copies, matching this guide's warning about what shallow copy does and doesn't protect).
  • Game engines — spawning many enemies/props from a small set of expensive-to-load templates (meshes, textures, physics colliders) is the textbook Prototype use case.
  • Document/template systems — "Duplicate" in an editor (a slide template, a spreadsheet template) is Prototype: copy an existing fully-configured document and let the user edit the copy.
  • Configuration objects in test suites — a baseline TestConfig prototype cloned and tweaked per test case instead of rebuilding the entire configuration from scratch each time.
  • Immutable value objects with "wither" methods (withName(...), common in modern Java records/builders) — a lightweight cousin of Prototype: copy-with-one-field-changed instead of full reconstruction.

Interview Questions

  • What problem does Prototype solve that a plain constructor or factory doesn't?
  • Explain shallow vs. deep copy with a concrete example of a bug shallow copy introduces.
  • Why is Java's Cloneable interface considered a broken/awkward contract? Name at least three specific issues.
  • Why does Effective Java recommend copy constructors over clone()? What do you gain?
  • What is a Prototype Registry, and how does it relate to Factory Method?
  • When would you reach for serialization-based deep cloning instead of a hand-written copy constructor, and what's the cost of doing so?
  • Give a real-world example where Prototype is clearly the better choice over Factory, and explain what makes construction "expensive" in that case.
  • If a class has both immutable and mutable fields, which ones need to be deep-copied during cloning, and why?