Skip to main content

Command Palette

Search for a command to run...

Prototype Pattern

Updated
10 min read
Prototype Pattern

The Prototype Pattern is a creational design pattern that allows you to create new objects by copying existing ones, rather than creating them from scratch. Think of it as a "copy machine" for objects—when you need a new instance, you simply clone an existing prototype instead of going through expensive initialization processes.

The Problem

Imagine you're building a game with complex character objects. Each character has dozens of properties: stats, equipment, skills, animations, and more. Creating a new character from scratch every time is:

  • Expensive: Initialization involves multiple database queries, file I/O, and complex calculations

  • Repetitive: Most characters share similar base configurations

  • Tightly Coupled: Your code becomes dependent on concrete classes and their constructors

// The hard way - expensive initialization
Warrior warrior1 = new Warrior();
warrior1.loadAnimations();
warrior1.loadTextures();
warrior1.initializeStats();
warrior1.loadEquipment();
// ... 50 more lines of setup

Warrior warrior2 = new Warrior();
// Repeat all the expensive operations again!

The Solution: Prototype Pattern

Instead of creating objects from scratch, we clone a pre-configured prototype. The pattern delegates the cloning process to the objects themselves.

Key Components

  1. Prototype Interface: Declares the cloning method

  2. Concrete Prototype: Implements the cloning method

  3. Client: Creates new objects by asking a prototype to clone itself

Real-World Implementation

Example 1: Game Character System

import java.util.*;

// Prototype interface
interface CharacterPrototype extends Cloneable {
    CharacterPrototype clone();
}

// Concrete Prototype
class GameCharacter implements CharacterPrototype {
    private Map<String, String> animations;
    private Map<String, String> textures;
    private Map<String, Integer> stats;
    private List<String> equipment;
    private List<String> skills;
    private String name;
    
    public GameCharacter() {
        this.animations = new HashMap<>();
        this.textures = new HashMap<>();
        this.stats = new HashMap<>();
        this.equipment = new ArrayList<>();
        this.skills = new ArrayList<>();
    }
    
    // Expensive initialization
    public void initialize() {
        System.out.println("Loading animations...");
        this.animations = loadAnimations();
        
        System.out.println("Loading textures...");
        this.textures = loadTextures();
        
        System.out.println("Initializing stats...");
        this.stats.put("health", 100);
        this.stats.put("mana", 50);
        this.stats.put("strength", 10);
        this.stats.put("agility", 8);
        
        System.out.println("Loading equipment...");
        this.equipment = loadEquipment();
    }
    
    private Map<String, String> loadAnimations() {
        // Simulate expensive operation
        Map<String, String> anims = new HashMap<>();
        anims.put("walk", "walk.anim");
        anims.put("attack", "attack.anim");
        return anims;
    }
    
    private Map<String, String> loadTextures() {
        // Simulate expensive operation
        Map<String, String> tex = new HashMap<>();
        tex.put("body", "body.png");
        tex.put("armor", "armor.png");
        return tex;
    }
    
    private List<String> loadEquipment() {
        // Simulate expensive operation
        return new ArrayList<>(Arrays.asList("sword", "shield"));
    }
    
    // The magic: clone method
    @Override
    public GameCharacter clone() {
        GameCharacter cloned = new GameCharacter();
        cloned.animations = new HashMap<>(this.animations);
        cloned.textures = new HashMap<>(this.textures);
        cloned.stats = new HashMap<>(this.stats);
        cloned.equipment = new ArrayList<>(this.equipment);
        cloned.skills = new ArrayList<>(this.skills);
        return cloned;
    }
    
    public void customize(String name, Map<String, Integer> statsModifier) {
        this.name = name;
        statsModifier.forEach((key, value) -> {
            this.stats.put(key, this.stats.getOrDefault(key, 0) + value);
        });
    }
    
    public String getName() {
        return name;
    }
}

// Usage
public class PrototypeDemo {
    public static void main(String[] args) {
        GameCharacter warriorPrototype = new GameCharacter();
        warriorPrototype.initialize(); // Expensive - done once!
        
        // Create multiple warriors by cloning
        GameCharacter warrior1 = warriorPrototype.clone();
        Map<String, Integer> aragornStats = new HashMap<>();
        aragornStats.put("strength", 5);
        warrior1.customize("Aragorn", aragornStats);
        
        GameCharacter warrior2 = warriorPrototype.clone();
        Map<String, Integer> boromirStats = new HashMap<>();
        boromirStats.put("strength", 3);
        boromirStats.put("health", 20);
        warrior2.customize("Boromir", boromirStats);
        
        GameCharacter warrior3 = warriorPrototype.clone();
        Map<String, Integer> gimliStats = new HashMap<>();
        gimliStats.put("strength", 7);
        gimliStats.put("health", 15);
        warrior3.customize("Gimli", gimliStats);
    }
}

Example 2: Document Templates

import java.util.*;

class DocumentStyle {
    private String font;
    private int fontSize;
    private String color;
    
    public DocumentStyle(String font, int fontSize, String color) {
        this.font = font;
        this.fontSize = fontSize;
        this.color = color;
    }
    
    public DocumentStyle(DocumentStyle other) {
        this.font = other.font;
        this.fontSize = other.fontSize;
        this.color = other.color;
    }
}

class Document implements Cloneable {
    private String header;
    private String footer;
    private DocumentStyle styles;
    private List<String> sections;
    
    public Document() {
        this.header = "";
        this.footer = "";
        this.sections = new ArrayList<>();
    }
    
    @Override
    public Document clone() {
        Document cloned = new Document();
        cloned.header = this.header;
        cloned.footer = this.footer;
        cloned.styles = new DocumentStyle(this.styles);
        cloned.sections = new ArrayList<>(this.sections);
        return cloned;
    }
    
    public void setContent(List<String> content) {
        this.sections = new ArrayList<>(content);
    }
    
    public void setHeader(String header) {
        this.header = header;
    }
    
    public void setFooter(String footer) {
        this.footer = footer;
    }
    
    public void setStyles(DocumentStyle styles) {
        this.styles = styles;
    }
}

// Usage
public class DocumentTemplateDemo {
    public static void main(String[] args) {
        // Create templates
        Document reportTemplate = new Document();
        reportTemplate.setHeader("Company Report");
        reportTemplate.setFooter("© 2026 Company Inc.");
        reportTemplate.setStyles(new DocumentStyle("Arial", 12, "#333"));
        
        // Clone for different reports
        Document salesReport = reportTemplate.clone();
        salesReport.setContent(Arrays.asList("Q1 Sales Data", "Revenue: $1M"));
        
        Document marketingReport = reportTemplate.clone();
        marketingReport.setContent(Arrays.asList("Campaign Results", "ROI: 250%"));
    }
}

Workflow Diagram

Deep vs Shallow Copy

A critical consideration when implementing the Prototype Pattern:

class Config {
    private String theme;
    
    public Config(String theme) {
        this.theme = theme;
    }
    
    public Config(Config other) {
        this.theme = other.theme;
    }
    
    public String getTheme() {
        return theme;
    }
    
    public void setTheme(String theme) {
        this.theme = theme;
    }
}

class ShallowCopyExample {
    private String name;
    private Config config;
    
    public ShallowCopyExample() {
        this.name = "Original";
        this.config = new Config("dark");
    }
    
    // Shallow clone - shares config reference
    public ShallowCopyExample shallowClone() {
        ShallowCopyExample clone = new ShallowCopyExample();
        clone.name = this.name;
        clone.config = this.config; // Same reference!
        return clone;
    }
    
    // Deep clone - creates new config
    public ShallowCopyExample deepClone() {
        ShallowCopyExample clone = new ShallowCopyExample();
        clone.name = this.name;
        clone.config = new Config(this.config); // New object!
        return clone;
    }
    
    public Config getConfig() {
        return config;
    }
}

// Usage
public class CopyDemo {
    public static void main(String[] args) {
        ShallowCopyExample original = new ShallowCopyExample();
        
        // Shallow copy problem
        ShallowCopyExample shallow = original.shallowClone();
        shallow.getConfig().setTheme("light");
        System.out.println(original.getConfig().getTheme()); // "light" - Oops!
        
        // Deep copy solution
        ShallowCopyExample deep = original.deepClone();
        deep.getConfig().setTheme("blue");
        System.out.println(original.getConfig().getTheme()); // "light" - Safe!
    }
}

When to Use the Prototype Pattern

✅ Use When

  1. Object creation is expensive: Database queries, file I/O, network calls

  2. Objects share similar configurations: Templates, presets, defaults

  3. You need to avoid subclass explosion: Instead of 100 subclasses, use a few prototypes

  4. Runtime object composition: Build objects dynamically based on user input

  5. Decoupling from concrete classes: Client doesn't need to know the exact class

❌ Avoid When

  1. Objects are simple: Creating from scratch is trivial

  2. Deep copying is complex: Circular references, complex object graphs

  3. Unique initialization required: Each object needs different setup

  4. Immutable objects: No benefit in cloning

Prototype Registry Pattern

For managing multiple prototypes:

import java.util.HashMap;
import java.util.Map;

class PrototypeRegistry {
    private Map<String, CharacterPrototype> prototypes;
    
    public PrototypeRegistry() {
        this.prototypes = new HashMap<>();
    }
    
    public void register(String key, CharacterPrototype prototype) {
        prototypes.put(key, prototype);
    }
    
    public void unregister(String key) {
        prototypes.remove(key);
    }
    
    public CharacterPrototype clone(String key) {
        CharacterPrototype prototype = prototypes.get(key);
        if (prototype == null) {
            throw new IllegalArgumentException("Prototype '" + key + "' not found");
        }
        return prototype.clone();
    }
}

// Usage
public class RegistryDemo {
    public static void main(String[] args) {
        PrototypeRegistry registry = new PrototypeRegistry();
        
        GameCharacter magePrototype = new GameCharacter();
        magePrototype.initialize();
        Map<String, Integer> mageStats = new HashMap<>();
        mageStats.put("mana", 150);
        magePrototype.customize("Mage", mageStats);
        
        GameCharacter roguePrototype = new GameCharacter();
        roguePrototype.initialize();
        Map<String, Integer> rogueStats = new HashMap<>();
        rogueStats.put("agility", 12);
        roguePrototype.customize("Rogue", rogueStats);
        
        registry.register("mage", magePrototype);
        registry.register("rogue", roguePrototype);
        
        // Create characters from registry
        GameCharacter mage1 = (GameCharacter) registry.clone("mage");
        GameCharacter rogue1 = (GameCharacter) registry.clone("rogue");
    }
}

Comparison with Other Patterns

Pattern When to Use Key Difference
Factory Method Need to delegate instantiation to subclasses Uses inheritance
Abstract Factory Need to create families of related objects Creates multiple types
Prototype Need to copy existing objects Uses cloning
Builder Need to construct complex objects step-by-step Focuses on construction process

Advanced Example: Shape Editor

import java.util.ArrayList;
import java.util.List;

// Abstract Prototype
abstract class Shape implements Cloneable {
    protected int x;
    protected int y;
    protected String color;
    
    public Shape() {
        this.x = 0;
        this.y = 0;
        this.color = "#000000";
    }
    
    public abstract Shape clone();
    public abstract void draw();
    
    public void setPosition(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public void setColor(String color) {
        this.color = color;
    }
}

// Concrete Prototypes
class Circle extends Shape {
    private int radius;
    
    public Circle() {
        super();
        this.radius = 10;
    }
    
    @Override
    public Circle clone() {
        Circle cloned = new Circle();
        cloned.x = this.x;
        cloned.y = this.y;
        cloned.color = this.color;
        cloned.radius = this.radius;
        return cloned;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing circle at (" + x + ", " + y + ") with radius " + radius);
    }
    
    public void setRadius(int radius) {
        this.radius = radius;
    }
}

class Rectangle extends Shape {
    private int width;
    private int height;
    
    public Rectangle() {
        super();
        this.width = 20;
        this.height = 10;
    }
    
    @Override
    public Rectangle clone() {
        Rectangle cloned = new Rectangle();
        cloned.x = this.x;
        cloned.y = this.y;
        cloned.color = this.color;
        cloned.width = this.width;
        cloned.height = this.height;
        return cloned;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing rectangle at (" + x + ", " + y + ") " + width + "x" + height);
    }
    
    public void setDimensions(int width, int height) {
        this.width = width;
        this.height = height;
    }
}

// Shape Editor
class ShapeEditor {
    private List<Shape> shapes;
    
    public ShapeEditor() {
        this.shapes = new ArrayList<>();
    }
    
    public void addShape(Shape shape) {
        shapes.add(shape);
    }
    
    public Shape cloneShape(int index) {
        if (index >= 0 && index < shapes.size()) {
            Shape cloned = shapes.get(index).clone();
            cloned.setPosition(cloned.x + 10, cloned.y + 10); // Offset the clone
            shapes.add(cloned);
            return cloned;
        }
        return null;
    }
    
    public void render() {
        for (Shape shape : shapes) {
            shape.draw();
        }
    }
}

// Usage
public class ShapeEditorDemo {
    public static void main(String[] args) {
        ShapeEditor editor = new ShapeEditor();
        
        Circle circle = new Circle();
        circle.setPosition(50, 50);
        circle.setRadius(25);
        circle.setColor("#FF0000");
        
        editor.addShape(circle);
        editor.cloneShape(0); // Clone the circle
        editor.cloneShape(0); // Clone again
        
        Rectangle rect = new Rectangle();
        rect.setPosition(100, 100);
        editor.addShape(rect);
        
        editor.render();
    }
}

Benefits

  1. Performance: Avoid expensive initialization

  2. Flexibility: Add/remove prototypes at runtime

  3. Reduced Subclassing: Clone and customize instead of creating subclasses

  4. Encapsulation: Cloning logic is encapsulated in the prototype

  5. Dynamic Configuration: Build objects based on runtime state

Drawbacks

  1. Complex Cloning: Deep copying can be tricky with circular references

  2. Clone Method Maintenance: Must update clone() when adding fields

  3. Not Always Obvious: When to use cloning vs construction isn't always clear

Best Practices

  1. Implement Deep Copy Carefully: Use copy constructors or implement Cloneable interface properly

  2. Document Clone Behavior: Make it clear whether you're doing shallow or deep copy

  3. Use Prototype Registry: For managing multiple prototype types

  4. Consider Serialization: Use serialization/deserialization for deep clones of complex objects

  5. Test Clone Independence: Ensure clones don't share mutable state

  6. Override clone() properly: Always override Object.clone() and handle CloneNotSupportedException

  7. Prefer Copy Constructors: In Java, copy constructors are often clearer than clone()

Conclusion

The Prototype Pattern is your go-to solution when object creation is expensive and you need multiple similar instances. By cloning pre-configured prototypes, you gain performance, flexibility, and cleaner code. Just remember to handle deep vs shallow copying appropriately, and you'll be cloning like a pro!


Remember: The Prototype Pattern is about efficiency through replication. When you find yourself repeatedly creating similar objects with expensive initialization, it's time to prototype! 🚀

👋 Before You Go...

If you enjoyed learning about the Prototype Pattern, feel free to clone this knowledge and share it with your fellow developers! Just remember: unlike objects, sharing knowledge doesn't require a deep copy—it's always better when it's shared by reference! 😄

P.S. - If you're still creating objects from scratch every time, your CPU called... it wants a break! 💻😅

Happy Cloning! 🎯✨