Prototype pattern
Open in the interactive app: practice, quiz, mock interview →In one minute
- The prototype pattern creates new objects by copying a configured example instead of calling a constructor. The caller says "give me another one like this" and needs to know neither the concrete class nor the steps it took to set the example up.
- It pays off when an object is expensive to set up (parsed, validated, resolved) and you need many variations of it: job templates, game entities, test fixtures, pod templates.
- The whole pattern lives or dies on copy semantics. A shallow copy shares nested lists and maps with the original, so editing the "new" object silently edits the template. A correct deep copy must also keep shared references shared and survive cycles.
- JavaScript's prototype chain is a different thing: delegation (lookups fall through to a parent object), not copying.
- Agents write
new = templateor{...template}and call it a copy. Review asks: which fields are nested, who else holds them, and which test proves the template is untouched after a clone is edited?
How it works
The problem
A CI system lets teams define job templates: a base image, environment variables, steps, resource limits and caches. Building a "Python service" template means parsing 40 lines of YAML, validating them and resolving image digests from a registry, about 30 ms. Every pull request creates 6 jobs from it, each changing a field or two. Calling the constructor per job repeats that work, and the code creating jobs has to know every field the template sets. What the job runner wants is simpler: "take the validated template and give me a private copy I can tweak."
The shape
The template object knows how to copy itself, and a registry holds the named templates:
registry.register("py-service", load_and_validate("py-service.yaml"))
job = registry.create("py-service") # a private deep copy
job.env["PYTHON"] = "3.13"
job.steps.append(Step("pytest -k shard_3"))create returns whatever concrete type the template is, the same decoupling a factory gives (see the Factory Method lesson), except that the recipe is data held in an existing instance rather than code in a function. New kinds of object can be registered at runtime, for example from a config file, without writing a class.
Shallow, deep and structural sharing
There are three ways to copy, and choosing one is the real design decision.
A shallow copy duplicates the outer object and copies its fields by reference: Python's copy.copy and dict.copy(), JavaScript's {...obj}, struct assignment in Go. Immutable fields are safe to share. Mutable ones are not: job.env in the copy is the template's dict, so the first job's PYTHON=3.13 leaks into every later job.
A deep copy walks the graph and copies every reachable mutable object. Done naively it has two bugs: it turns one shared object into two (two steps that shared one cache config now have separate ones), and it recurses forever on a cycle (a step pointing back at its job). Python's copy.deepcopy fixes both with a memo keyed by id(obj), checked before copying and filled before recursing into children. The Practice builds exactly this.
Structural sharing avoids copying by making the template immutable. A "copy with one change" builds a new top-level object pointing at the unchanged parts of the old one. Python's dataclasses.replace and copy.replace (3.13) do this for one level; persistent collections (Clojure's vectors, Immutable.js) do it for whole trees, copying only the path to the change.
Language support, and where it breaks
- Python offers
__copy__and__deepcopy__(self, memo)hooks and otherwise falls back to the pickle protocol. Objects holding sockets, locks or files either fail to copy or produce a copy sharing the handle. - JavaScript has
structuredClone, which handles cycles,Map,SetandDate, but throws on functions and returns plain objects for class instances, dropping their methods.JSON.parse(JSON.stringify(x))is worse: it dropsundefined, stringifies dates and throws on cycles. - Go has no generic clone. Struct assignment copies fields, but slices, maps and pointers inside still alias.
slices.Cloneandmaps.Clone(Go 1.21) are shallow. A correctClone()is written by hand. - Java's
Cloneablehas noclonemethod andObject.cloneis protected and shallow; Effective Java advises a copy constructor or copy factory instead.
When not to use it
- Construction is cheap and has few fields. A constructor call or an options object is clearer than a registry of examples.
- The template could be immutable. Then share it and use "copy with changes" at the top level; no deep clone is needed.
- The object owns resources. A database connection, an open file or a running goroutine is not a value. Clone the configuration and open a new resource from it.
Reviewing agent-written code
Asked to "create jobs from a template", an agent will often write const job = { ...template } and then job.steps.push(...). Tests that build one job pass; production breaks when the second job inherits the first job's step. Ask for a test that creates two objects from one template, mutates nested fields of the first, and asserts the template and the second are unchanged. A hand-written recursive clone without a memo is a stack overflow waiting for the first cyclic template.
The same shape elsewhere
Prototype is the duplicates problem shape seen from the other side: you want many copies, and the bugs are the parts that should not have been duplicated or shared. Infrastructure stamps out copies of a validated example constantly. A Kubernetes Deployment holds a pod template that the ReplicaSet controller copies into every pod; VMs boot from golden images; fork() copies a process; AWS Lambda SnapStart restores new environments from a snapshot of an initialised one. All share the shallow-copy failure mode: something that should be unique per copy (a random seed, a host key, a connection) was baked into the template and is now shared by every instance.
Down the stack
Layer 1The contract
clone() returns a new object of the same concrete type, equal to the original at the moment of copying. Afterwards, mutating any part the contract calls "owned" must not affect the original, and vice versa. The contract also names deliberately shared fields (immutable values, handles such as a logger) and how sharing and cycles are treated: a graph-preserving clone maps two references to one child onto one copied child, and a cycle onto a cycle. A registry promises to store a snapshot taken at registration, so later edits to the object passed in do not change future clones.
Layer 2The mechanism
CPython's copy.deepcopy looks up a copier in _deepcopy_dispatch by exact type (int and str return themselves; list, dict and tuple have dedicated functions), then tries __deepcopy__, then __reduce_ex__(4). It stores each new object in memo[id(original)] before copying children, and keeps originals alive in the memo so their id values cannot be reused mid-copy. structuredClone uses the same serialisation as postMessage: V8's ValueSerializer writes the graph to a buffer, turning repeat visits into back-references, and ValueDeserializer rebuilds it. Go's struct assignment is a memmove of the struct's bytes; a slice header (pointer, length, capacity: 24 bytes) is copied, its backing array is not. JavaScript's prototype chain copies nothing: Object.create(proto) sets the new object's [[Prototype]] slot, and property reads that miss walk up the chain.
Layer 3The machine
A shallow copy is one allocation plus a field copy: tens of nanoseconds. A deep copy allocates once per reachable mutable node, so a template with 2,000 nested dicts and lists costs 2,000 allocations and, in CPython, several milliseconds of per-node dispatch and memo inserts. That can exceed building from scratch, so measure before assuming cloning is cheap. The clone's nodes also land wherever the allocator has space, losing the original's locality.
The kernel uses the cheapest prototype of all. fork() copies page tables, not memory, and marks pages read-only in both processes; the first write to a page faults and copies that one 4 KiB page. Redis BGSAVE relies on this: the child snapshots the dataset while the parent keeps writing, and extra memory equals the pages touched during the save, which on a write-heavy instance approaches the whole dataset. Copy-on-write is structural sharing implemented by the MMU.
Why it's built this way
- Some objects are expensive to set up or carry many decisions (validation, lookups, defaults), and you need many variations of them.
- Rebuilding each one repeats that cost and spreads knowledge of every field into the code that creates them, so copy a finished example instead.
- Copying by reference is cheap but makes the copies share mutable state, so owned nested data must be copied deeply.
- Deep copies that duplicate shared nodes or loop on cycles corrupt the graph, so the copier keeps a memo from original to copy.
- Deep copies cost allocations proportional to the graph, so where the template can be immutable, share it and copy only what changes.
Diagram
flowchart TD
R["registry.create(key)"] --> L{"key registered?"}
L -- "no" --> E["error: unknown prototype"]
L -- "yes" --> C["clone(node, memo)"]
C --> M{"id(node) in memo?"}
M -- "yes" --> S["return memo copy (shared or cycle)"]
M -- "no" --> N["new node, copy scalar fields"]
N --> W["memo[id] = new node"]
W --> K["clone each child with same memo"]
K --> O["apply overrides to root"]
O --> U["caller gets private copy"]Worked example
A game spawns enemies from data files holding stats, an equipment list, a 120-node behaviour tree and a reference to a shared texture atlas. Building one from its file takes 1.8 ms, so a wave of 300 costs 540 ms, far beyond a 16.7 ms frame budget at 60 fps.
The team loads each type once and spawns with clone(prototype). The first version used a generic deep copy, which also copied the 4 MB atlas metadata per enemy: a 1.2 GB spike. The fix was an explicit clone contract: stats and equipment are copied, the behaviour tree is shared because it is immutable after load (per-enemy state lives in a small "blackboard"), and asset references are shared handles. A spawn now copies about 2 KB in 6 microseconds; a wave takes under 2 ms.
The bug that slipped through came from the other direction: the hand-written clone copied the equipment list but not the dicts inside it, so one goblin's power-up upgraded the sword of every goblin spawned after it. A test that clones twice and mutates nested data on one now guards every prototype type.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Constructor or factory per object | Explicit; no copy semantics to get wrong | Repeats expensive setup; caller knows fields | Setup is cheap and variants are few |
| Shallow copy | Fast; one allocation | Nested mutable data is shared with the template | All fields are immutable or meant to be shared |
Generic deep copy (deepcopy, structuredClone) | Correct for plain data, cycles and sharing | Slow on big graphs; copies handles you meant to share; drops class identity in JS | Plain data templates, tests, tooling |
Hand-written clone() | Precise contract per field; fastest | Must be updated when fields are added | Hot paths and objects mixing values with shared handles |
| Immutable template plus structural sharing | No copy cost; safe to share across threads | Requires an immutable data model | Configuration, documents, state in UI frameworks |
Pitfalls in production
- Shallow copies of templates with nested lists or maps, so edits leak into the template and every later copy. Fix: deep copy owned data, and test two clones with nested mutation.
- A new field added to a class but not to its hand-written
clone(), so it is silently shared or dropped. Fix: a test that compares a clone field by field using reflection or__dict__, so a new field fails it. - Values that must be unique per instance (ids, random seeds, SSH host keys on VM images) baked into the prototype. Fix: regenerate them in a post-clone hook, the way cloud-init regenerates host keys on first boot.
- Registry entries mutated after registration because the registry stored the caller's object. Fix: clone on
registeras well as oncreate. structuredCloneor JSON round-trips on TypeScript class instances, returning objects without methods. Fix: aclone()method, or templates as plain data.
Leadership lens
- Ask for the clone contract in writing: which fields are copied, which are shared, and why. Most prototype bugs are disagreements about that list.
- Prefer immutable templates. They remove both the copy cost and the aliasing bugs, and are safe to share across threads.
- Golden images and templates are a supply-chain asset. Decide who owns them and how fast a fix reaches every instance created from them.
- Make "clone twice, mutate one, check the other and the template" a standard test, and ask agents to write it before the clone.
Check yourself
A template holds a list of steps. Why does
{...template}cause a bug only when two objects are created from it?Show answer
The spread copies the reference to the list, so both copies and the template share one list. With one copy the shared mutation is invisible; the second copy reveals the first copy's changes.
Why does a correct deep copy need a memo of already-copied objects?
Show answer
Without it, a node reached twice is copied twice, so shared references become separate objects, and a cycle recurses forever. The memo maps each original to its single copy.
When is cloning slower than constructing from scratch?
Show answer
When the object graph is large and the constructor is cheap: a deep copy allocates and dispatches per node, while a constructor might share immutable parts or build compact data directly.
How is JavaScript's prototype chain different from the prototype pattern?
Show answer
The chain is delegation: a new object points at its prototype and reads that miss fall through to it, so changes to the prototype are visible later. The pattern copies, so later changes to the template do not affect existing copies.
Watch
- 10 Design Patterns Explained in 10 Minutes (Fireship): includes prototype in JavaScript and shows how it relates to the language's prototype chain.
Free resources
- The Prototype Pattern (Brandon Rhodes, python-patterns.guide): why Python rarely needs the class-based form, and what it uses instead.
- copy: shallow and deep copy operations (Python docs):
__copy__,__deepcopy__, the memo dictionary andcopy.replace. - structuredClone (MDN): what the browser and Node deep copy supports, and what it throws on.
- Inheritance and the prototype chain (MDN): how JavaScript's delegation works, to keep it apart from the copying pattern.
- Go Slices: usage and internals (Go blog): why copying a struct that holds a slice still shares the backing array.
- Pod templates (Kubernetes docs): the prototype pattern at infrastructure scale, including how template changes roll out.
- Redis persistence (Redis docs): how
BGSAVEuses fork and copy-on-write to snapshot a live dataset.