Skip to content
4 min read

🧬 Prototype Design Pattern

Create new objects by copying an existing instance (prototype) rather than building from scratch.


Real-World Analogy

Think of cell division (mitosis). Instead of building a new cell from raw amino acids (expensive!), an existing cell copies itself — duplicating all its internal structure. The clone can then mutate independently. Similarly, the Prototype pattern clones an existing configured object instead of going through costly creation steps.

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
    O["🧫 Original Cell"] -->|"clone!"| C1["🧫 Clone 1"] -->|"mutate"| M1["🦠 Variant A"]
    O -->|"clone!"| C2["🧫 Clone 2"] -->|"mutate"| M2["🦠 Variant B"]

    style O fill:#E8F5E9,stroke:#2E7D32,stroke-width:2px,color:#000
    style C1 fill:#E8F5E9,stroke:#2E7D32,color:#000
    style C2 fill:#E8F5E9,stroke:#2E7D32,color:#000
    style M1 fill:#FFF8E1,stroke:#F9A825,color:#000
    style M2 fill:#FCE4EC,stroke:#C62828,color:#000

🏗️ Structure

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
    A["🟢 Client"] -->|"lookup"| G["📋 Registry"]
    G -->|"returns"| B[["🧬 Prototype"]]
    B -->|"extends"| C{{"📄 Document"}}
    B -->|"extends"| D{{"🎨 Shape"}}
    C -->|"clone"| E(["📄 Doc Copy"])
    D -->|"clone"| F(["🎨 Shape Copy"])

    style A fill:#E8F5E9,stroke:#2E7D32,color:#000
    style G fill:#FCE4EC,stroke:#C62828,color:#000
    style B fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#000
    style C fill:#E3F2FD,stroke:#1565C0,color:#000
    style D fill:#E3F2FD,stroke:#1565C0,color:#000
    style E fill:#FFF8E1,stroke:#F9A825,color:#000
    style F fill:#FFF8E1,stroke:#F9A825,color:#000

UML Class Diagram

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
    class Prototype {
        <<interface>>
        +clone() Prototype
    }
    class Employee {
        -name : String
        -department : String
        -address : Address
        -skills : List~String~
        +clone() Employee
        +shallowClone() Employee
    }
    class Address {
        -street : String
        -city : String
        -country : String
    }
    class ShapePrototypeRegistry {
        -prototypes : Map~String, Shape~
        +get(String key) Shape
        +register(String key, Shape prototype) void
    }

    Employee ..|> Prototype
    Employee *-- Address : contains
    ShapePrototypeRegistry o-- Prototype : stores

    style Prototype fill:#FFF3E0,stroke:#E65100,color:#000
    style Employee fill:#E8F5E9,stroke:#2E7D32,color:#000
    style Address fill:#E3F2FD,stroke:#1565C0,color:#000
    style ShapePrototypeRegistry fill:#F3E5F5,stroke:#6A1B9A,color:#000

❓ The Problem

Without This Pattern

Java
public class SpreadsheetApp {
    public void duplicateRow(String rowId) {
        // Must re-fetch and re-configure from scratch every time
        Row original = database.fetchRow(rowId);       // 200ms DB call
        CellFormat format = styleService.resolve(rowId); // 100ms API call
        List<Formula> formulas = parser.parse(original); // 50ms computation

        // Creating a "copy" means repeating ALL the expensive work
        Row copy = database.fetchRow(rowId);             // 200ms AGAIN
        CellFormat copyFmt = styleService.resolve(rowId);// 100ms AGAIN
        List<Formula> copyFormulas = parser.parse(copy); // 50ms AGAIN
        copy.setFormulas(copyFormulas);
        copy.setFormat(copyFmt);
    }

    public void createShapeFromToolbar(Shape selected) {
        // Don't know concrete type at compile time!
        if (selected instanceof Circle c) {
            return new Circle(c.getRadius(), c.getColor(), c.getX(), c.getY());
        } else if (selected instanceof Rectangle r) {
            return new Rectangle(r.getWidth(), r.getHeight(), r.getColor());
        }
        // Every new shape type = another else-if branch...
    }
}

Problems:

  • Costly re-initialization — duplicating an object means repeating expensive DB calls, API requests, and computations that were already done for the original
  • Tight coupling to concrete types — creating "another one like this" requires if-else chains checking instanceof, violating the Open/Closed Principle every time a new type is added
  • Complex setup cannot be replayed — if the object was configured through 10 interactive steps (user input, calibration), there is no way to replay that process programmatically
  • Performance bottleneck — in scenarios like game engines spawning 1000 enemies or load tests creating 50 user templates, re-creating from scratch makes the operation O(n * creation_cost) instead of O(n * copy_cost)
  • No polymorphic duplication — without a clone() contract, the client must know every concrete class to duplicate it, breaking abstraction

✅ The Solution

The Prototype pattern solves this by:

  1. Declaring a clone() method in a prototype interface
  2. Each concrete class implements its own cloning logic
  3. Client code clones the prototype instead of constructing from scratch
  4. Optionally, a Prototype Registry stores pre-configured prototypes for lookup

This shifts the cost from creation to copying — which is almost always cheaper.


🛠️ Implementation

Java
// Prototype Interface
public interface Prototype<T> {
    T clone();
}

// ========== Shallow Copy Example ==========
public class Address {
    private String street;
    private String city;
    private String country;

    public Address(String street, String city, String country) {
        this.street = street;
        this.city = city;
        this.country = country;
    }

    // Getters and setters
    public String getStreet() { return street; }
    public void setStreet(String street) { this.street = street; }
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
    public String getCountry() { return country; }
}

public class Employee implements Prototype<Employee> {
    private String name;
    private String department;
    private Address address;        // Reference type!
    private List<String> skills;    // Reference type!

    public Employee(String name, String department, Address address, 
                    List<String> skills) {
        this.name = name;
        this.department = department;
        this.address = address;
        this.skills = skills;
    }

    // SHALLOW COPY — address and skills are shared references!
    public Employee shallowClone() {
        return new Employee(name, department, address, skills);
    }

    // DEEP COPY — all nested objects are independently copied
    @Override
    public Employee clone() {
        Address clonedAddress = new Address(
            address.getStreet(), address.getCity(), address.getCountry()
        );
        List<String> clonedSkills = new ArrayList<>(skills);
        return new Employee(name, department, clonedAddress, clonedSkills);
    }

    // Getters and setters
    public void setName(String name) { this.name = name; }
    public String getName() { return name; }
    public Address getAddress() { return address; }
    public List<String> getSkills() { return skills; }
}

// ========== Usage ==========
public class Demo {
    public static void main(String[] args) {
        Employee original = new Employee(
            "Alice", "Engineering",
            new Address("123 Main St", "San Jose", "USA"),
            new ArrayList<>(List.of("Java", "Spring", "AWS"))
        );

        // Deep clone — independent copy
        Employee clone = original.clone();
        clone.setName("Bob");
        clone.getAddress().setStreet("456 Oak Ave");
        clone.getSkills().add("Kubernetes");

        // Original is NOT affected
        System.out.println(original.getName());           // Alice
        System.out.println(original.getAddress().getStreet()); // 123 Main St
        System.out.println(original.getSkills().size());  // 3
    }
}

Shallow vs Deep Copy

  • Shallow Copy: Copies primitive values, but references point to the SAME objects. Modifying nested objects in the clone affects the original!
  • Deep Copy: Recursively copies ALL nested objects. Clone is fully independent.
  • Always use Deep Copy unless you explicitly want shared state.

Pre-configure prototypes and retrieve them by key.

Java
public class ShapePrototypeRegistry {
    private final Map<String, Shape> prototypes = new HashMap<>();

    public ShapePrototypeRegistry() {
        // Pre-configure expensive prototypes
        Circle defaultCircle = new Circle();
        defaultCircle.setColor("red");
        defaultCircle.setRadius(10);
        prototypes.put("red-circle", defaultCircle);

        Rectangle defaultRect = new Rectangle();
        defaultRect.setColor("blue");
        defaultRect.setWidth(100);
        defaultRect.setHeight(50);
        prototypes.put("blue-rectangle", defaultRect);

        // Complex shape that takes expensive computation
        FractalShape fractal = new FractalShape();
        fractal.computeIterations(10000);  // Expensive!
        prototypes.put("fractal", fractal);
    }

    public Shape get(String key) {
        Shape prototype = prototypes.get(key);
        if (prototype == null) {
            throw new IllegalArgumentException("Unknown prototype: " + key);
        }
        return prototype.clone();  // Always return a copy!
    }

    public void register(String key, Shape prototype) {
        prototypes.put(key, prototype);
    }
}

// Usage — get copies without expensive setup
ShapePrototypeRegistry registry = new ShapePrototypeRegistry();

Shape circle1 = registry.get("red-circle");      // Instant clone
Shape circle2 = registry.get("red-circle");      // Another instant clone
Shape fractal = registry.get("fractal");          // Skip 10000 iterations!

Java's built-in mechanism — works but has design flaws.

Java
public class Document implements Cloneable {
    private String title;
    private String content;
    private List<String> authors;
    private Map<String, String> metadata;

    public Document(String title, String content) {
        this.title = title;
        this.content = content;
        this.authors = new ArrayList<>();
        this.metadata = new HashMap<>();
    }

    public void addAuthor(String author) { authors.add(author); }
    public void addMetadata(String key, String value) { metadata.put(key, value); }

    @Override
    public Document clone() {
        try {
            Document clone = (Document) super.clone();  // Shallow copy
            // Deep copy mutable fields
            clone.authors = new ArrayList<>(this.authors);
            clone.metadata = new HashMap<>(this.metadata);
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError("Should not happen", e);
        }
    }

    @Override
    public String toString() {
        return "Document{title='" + title + "', authors=" + authors + "}";
    }
}

// Usage
Document original = new Document("Design Patterns", "Chapter 1...");
original.addAuthor("Gang of Four");
original.addMetadata("version", "1.0");

Document copy = original.clone();
copy.addAuthor("New Author");

System.out.println(original);  // authors=[Gang of Four]
System.out.println(copy);      // authors=[Gang of Four, New Author]

Problems with Java's Cloneable

  • Cloneable is a marker interface — doesn't declare clone()
  • Object.clone() does shallow copy by default
  • Must manually deep copy all mutable fields
  • CloneNotSupportedException is a checked exception (awkward)
  • Joshua Bloch: "Cloneable is broken. Use copy constructors or copy factories instead."

The cleanest modern approach — no Cloneable needed.

Java
public class GameState {
    private int score;
    private int level;
    private List<String> inventory;
    private Map<String, Integer> achievements;

    // Regular constructor
    public GameState(int level) {
        this.score = 0;
        this.level = level;
        this.inventory = new ArrayList<>();
        this.achievements = new HashMap<>();
    }

    // Copy constructor — explicit and clear
    public GameState(GameState other) {
        this.score = other.score;
        this.level = other.level;
        this.inventory = new ArrayList<>(other.inventory);
        this.achievements = new HashMap<>(other.achievements);
    }

    // Copy factory method — alternative style
    public static GameState copyOf(GameState other) {
        return new GameState(other);
    }

    // Mutators
    public void addScore(int points) { score += points; }
    public void addItem(String item) { inventory.add(item); }
    public void unlock(String achievement) { achievements.put(achievement, level); }
}

// Save game state before a risky action
GameState checkpoint = new GameState(currentState);  // Deep copy
try {
    currentState.enterBossFight();
} catch (GameOverException e) {
    currentState = checkpoint;  // Restore from copy
}

🎯 When to Use

  • When object creation is more expensive than copying (DB calls, network, computation)
  • When you need to create objects without knowing their concrete class at compile time
  • When objects have many shared configurations with minor variations
  • When you want to avoid subclass proliferation just to configure different instances
  • When you need undo/redo functionality — save snapshots via cloning
  • When reducing the number of initializations in performance-critical code

🌍 Real-World Examples

Framework / Library Prototype Usage
Object.clone() Java's built-in prototype mechanism
Spring Framework scope="prototype" — new instance per request
java.util.Arrays.copyOf() Creates copies of arrays
Apache Commons SerializationUtils.clone() — deep copy via serialization
Collections.unmodifiableList() Defensive copies of collections
JavaScript Object.create() — prototypal inheritance
Game Engines Spawn entities by cloning prefab templates

Pitfalls

  1. Shallow copy bugs — Forgetting to deep copy nested mutable objects leads to shared state corruption
  2. Circular references — Objects referencing each other can cause infinite loops during deep copy
  3. Final fields — Cannot reassign final fields in a clone (use constructor-based approach)
  4. Clone identity confusionclone != original but clone.equals(original) should be true; maintain contract
  5. Performance assumption — Cloning isn't always faster; for simple objects, new can be faster than copy
  6. Broken Cloneable — Java's Cloneable interface is poorly designed; prefer copy constructors

Key Takeaways

  • Prototype creates objects by cloning existing instances — avoids costly re-initialization
  • Always implement deep copy for objects with mutable reference fields
  • Prototype Registry pre-configures and caches expensive-to-create templates
  • In modern Java, prefer copy constructors or copy factory methods over Cloneable
  • Key interview distinction: Prototype avoids the new keyword complexity — client doesn't need to know the concrete class
  • Combines well with other patterns: use Prototype with Factory (factory clones a prototype) or Memento (save/restore state)