Prototype Pattern: Clone Objects Instead of Rebuilding
Create objects by cloning pre-configured instances instead of rebuilding them from scratch. Prototype pattern with Python, Java, and JavaScript examples.
Overview
A game spawns two hundred enemies that differ only in health, speed, and weapon. Rebuilding each one from a constructor means re-parsing config, re-resolving assets, and re-running the same initialization two hundred times. The Prototype pattern sidesteps that: keep one fully-built instance around, and stamp out new objects by copying it.
This is the catalog entry for the pattern as the Gang of Four defined it — what it’s for, when it pays off, and where it bites. For an applied walkthrough with a configuration registry, see the cloning recipe.
When to Use
Use the Prototype pattern when:
- Constructing an object is expensive: it parses files, hits the network, or wires up a large object graph.
- You need many objects that differ only slightly, like enemy variants or document templates.
- The concrete class to instantiate isn’t known until runtime — you hold a configured instance, not a class.
- You want to snapshot an object’s current state and keep mutating the original.
The runtime-type case is the one people miss: a plugin system that receives “an object to copy” can’t call new UnknownClass(), but it can call clone() on whatever instance it was handed. Prototype decouples “what to create” from “which class knows how”, which is something constructors can’t do.
Skip it when objects are cheap to build or immutable. If your objects are immutable value types, cloning buys you nothing; you can share the instance directly, which is what Java records and Python dataclasses with frozen=True are for.
Solution
Every implementation exposes a clone() method that returns a copy of the receiver. The subtlety is what “copy” means — shallow copies share nested objects, deep copies duplicate them.
Python
import copy
from abc import ABC, abstractmethod
class Document(ABC):
def __init__(self, content="", formatting=None):
self.content = content
self.formatting = formatting or {}
@abstractmethod
def clone(self):
pass
class Report(Document):
def __init__(self, content="", formatting=None, sections=None):
super().__init__(content, formatting)
self.sections = sections or []
def clone(self):
# Deep copy so nested structures stay independent
return copy.deepcopy(self)
# Build the prototype once
prototype = Report(
content="Annual Report Template",
formatting={"font": "Arial", "size": 12},
sections=["Introduction", "Financials", "Conclusion"],
)
# Stamp out variations
report_a = prototype.clone()
report_a.content = "2024 Annual Report"
report_a.sections.append("Appendix")
report_b = prototype.clone()
report_b.formatting["size"] = 14
print(report_a.formatting is report_b.formatting) # False — deep copy
copy.deepcopy handles nested structures and circular references, but it’s not free: it walks the entire object graph. For hot paths, implement __copy__/__deepcopy__ yourself and copy only the fields that actually need it.
JavaScript
class Report {
constructor(content = "", formatting = {}, sections = []) {
this.content = content;
this.formatting = formatting;
this.sections = sections;
}
clone() {
// structuredClone does a deep copy (browsers, Node 17+)
return structuredClone(this);
}
}
const prototype = new Report(
"Annual Report Template",
{ font: "Arial", size: 12 },
["Introduction", "Financials", "Conclusion"]
);
const reportA = prototype.clone();
reportA.sections.push("Appendix");
const reportB = prototype.clone();
reportB.formatting.size = 14;
console.log(reportA.formatting === reportB.formatting); // false
Watch the structuredClone limits: it copies Map, Set, Date, ArrayBuffer, and circular references, but it throws on functions, DOM nodes, and class instances lose their prototype chain: reportA instanceof Report comes back false. If the clone must keep methods, write clone() manually: return new Report(this.content, {...this.formatting}, [...this.sections]).
Java
public class Report implements Cloneable {
private String content;
private Map<String, Object> formatting;
private List<String> sections;
public Report(String content, Map<String, Object> formatting,
List<String> sections) {
this.content = content;
this.formatting = new HashMap<>(formatting);
this.sections = new ArrayList<>(sections);
}
@Override
public Report clone() {
// Copy the mutable fields; String is immutable so it can be shared
return new Report(content, new HashMap<>(formatting), new ArrayList<>(sections));
}
}
Java’s built-in Cloneable/Object.clone() does a shallow copy — cloned lists and maps stay shared. Write your own clone() or copy constructor that duplicates the mutable fields. Prefer copy constructors or static factories; Cloneable is widely considered a broken interface because it doesn’t actually declare clone().
Explanation
The pattern has two roles:
- Prototype — declares
clone()(an interface, a base class, or just a convention). - Concrete prototypes — implement
clone()to return a usable copy of themselves.
A prototype registry is the usual companion: a map of named prototypes that callers clone on demand, so the system doesn’t even need to know concrete classes, just the registry keys.
The hard part isn’t the method signature; it’s the copy semantics. A shallow copy duplicates the object but keeps references to the same nested objects; mutate reportA.formatting and reportB changes too. A deep copy duplicates the whole graph, which is safer but slower and fails on things that can’t be copied: open file handles, sockets, threads, lambda closures. Decide per field: share what’s immutable or expensive, copy what’s mutable.
Circular references deserve a mention because they break naive implementations: object A references B which references A, and a recursive clone loops forever or overflows the stack. copy.deepcopy and structuredClone both handle cycles correctly; a hand-rolled recursive copier usually doesn’t.
Here’s the shallow-copy bug in its smallest form — two “independent” clones sharing a list:
class Enemy:
def __init__(self, name, abilities):
self.name = name
self.abilities = abilities
def clone(self):
# Shallow: abilities is shared with every clone
return Enemy(self.name, self.abilities)
grunt = Enemy("grunt", ["melee"])
elite = grunt.clone()
elite.name = "elite"
elite.abilities.append("fireball")
print(grunt.abilities) # ['melee', 'fireball'] — the prototype changed too
One list(self.abilities) in clone() fixes it. That single line is the difference between “the pattern works” and “two hundred enemies share one ability list”.
Cost and limits
Cloning trades construction cost for copy cost, and the trade isn’t always in your favor. copy.deepcopy walks the whole object graph through reflection, so on a large tree it’s often slower than the constructor it was meant to avoid. structuredClone is faster but still linear in graph size. The pattern pays off when initialization does work that copying skips: parsing, I/O, dependency resolution. If construction is just field assignment, a constructor call beats a graph walk. Benchmark the two paths before adopting Prototype “for performance”; the usual result is that expensive setup amortizes, while cheap setup doesn’t.
The prototype registry
When a system keeps several named prototypes, the standard companion is a registry: a map from name to configured instance that returns a fresh clone on every lookup.
class ConfigRegistry {
private prototypes = new Map<string, ProductConfig>();
register(key: string, config: ProductConfig): void {
this.prototypes.set(key, config);
}
get(key: string): ProductConfig | undefined {
// Every caller gets an independent clone
return this.prototypes.get(key)?.clone();
}
}
const registry = new ConfigRegistry();
registry.register("pro", new ProductConfig("Pro", 29.99, "software"));
const custom = registry.get("pro");
custom?.setAttribute("discount", "20%"); // the registered prototype is untouched
Callers never see a concrete class or a constructor — just a key and a clone. That decoupling is the real payoff when object types are decided by configuration at runtime.
Where It Shows Up
The pattern hides inside frameworks you probably already use. Spring’s @Scope("prototype") gives every injection point a fresh bean instance instead of the shared singleton: prototype semantics without a clone() call. Unity’s Object.Instantiate(prefab) clones a configured prefab: that’s the entire engine workflow for spawning enemies, bullets, and particles. Python’s copy module exists precisely because this pattern is common enough to need stdlib support, and Java’s Cloneable is the cautionary tale of bolting it on afterwards: an interface that declares nothing and an Object.clone() that throws unless the marker interface is present.
Variants
| Variant | Use case | Trade-off |
|---|---|---|
| Shallow clone | Flat objects, or intentional shared state | Fast; mutations leak between clones |
| Deep clone | Nested mutable state | Slower; breaks on uncopyable resources |
| Prototype registry | Named, preconfigured prototypes | Central catalog; extra indirection |
| Serialization clone | Copy via serialize/deserialize | Handles graphs; slow, loses non-serializable state |
| Copy constructor | new Report(other) | Explicit and type-safe; one per class |
What Works
- Pick shallow vs deep per field. Copy mutable collections, share immutable scalars and expensive read-only resources.
- Implement
clone()explicitly instead of relying on language defaults — Java’sObject.clone()and JavaScript’s{...obj}are both shallow. - Keep prototypes immutable after registration. A registry whose entries get mutated produces clones of a moving target.
- Let immutable objects opt out. If the object can’t change, cloning is wasted work — return
this.
Common Mistakes
- Cloning something a caller will mutate back. If the prototype itself is editable after registration, every new clone inherits whatever the last mutation left behind. Treat registered prototypes as read-only.
- Shallow-copying mutable nested state. The classic Prototype bug:
reportA.sections.push("X")shows up inreportB. If clones must be independent, nested mutables need deep copies. - Assuming
structuredClonepreserves classes. It returns plain objects —instanceoffails and methods disappear. - Cloning live resources. File handles, DB connections, and sockets can’t be meaningfully cloned; re-open or re-create them inside
clone(). - Recursion into circular references. Hand-rolled deep copiers need a visited-set; the stdlib versions already have one.
- Reaching for Prototype when a factory fits. If creation is cheap and parameterized, a factory or builder is simpler than keeping a mutable prototype around.
Further Reading
- MDN: structuredClone() — what the deep clone copies and what throws.
- Refactoring.Guru: Prototype — the canonical catalog entry with diagrams.
- Python docs: copy module —
copyvsdeepcopysemantics. - Companion examples — runnable prototype registry in Python and JavaScript with tests.
Frequently Asked Questions
What's the difference between Prototype and Factory?
Factory creates objects through a dedicated creation method; Prototype creates them by copying an existing instance. Factory answers "build me an X with these parameters"; Prototype answers "give me another one like this". If construction is cheap, Factory is usually simpler.
When is cloning actually faster than constructing?
When initialization does I/O or heavy setup, like parsing a config file, loading a template, resolving a dependency graph. Cloning an in-memory object skips all of that. For plain objects with a few fields, the constructor wins every time; measure before adopting the pattern for performance.
Should I always deep-copy?
No. Deep copy is the safe default for mutable nested state, but it's slower and fails on uncopyable resources. If nested objects are immutable or shared state is intentional, shallow copy is correct — just document which one clone() performs so callers aren't surprised.
How does Prototype relate to Memento?
Both capture object state, but for different reasons: Prototype clones to produce new working objects, while Memento snapshots state to restore it later. If you're versioning or undoing, that's Memento's job.
Does JavaScript's prototype chain count as this pattern?
Not really: same word, different idea. The GoF pattern is about instance-level copying via clone(); JS's prototype chain is delegation for property lookup. structuredClone and spread-based copying are the JS mechanisms that actually implement the pattern.
How do I clone an object that holds a DB connection or open file?
You don't; you re-establish it. Non-copyable resources need to be re-acquired inside clone() (open a new connection, reopen the file) or excluded from the copy entirely. A cloned object pointing at the original's socket is a bug that only surfaces under load.
Related Resources
Factory Pattern
Create objects without specifying the exact class to instantiate. A creational design pattern for flexible object creation.
PatternBuilder Pattern
Construct complex objects step by step. A creational design pattern for readable, configurable object construction.
PatternSingleton Pattern: Java, Python & TypeScript Examples
Ensure a class has only one instance and provide global access to it — the creational design pattern for controlled object creation, with production-ready implementations.
PatternMemento Pattern
Capture and restore an object's internal state without violating encapsulation. A behavioral design pattern for undo/redo.
PatternPrototype Pattern for Object Cloning and Configuration
Create new objects by copying existing ones, allowing pre-configured templates and avoiding subclass explosion when object creation is expensive