Prototype Pattern
Concept
Prototype creates new objects by cloning an existing, fully-configured instance rather than building one from scratch through a constructor. The prototype object holds a clone() (or equivalent copy) method that produces a new, independent instance carrying the same state as the original — the client asks the prototype to copy itself instead of calling new SomeConcreteClass(...) and re-supplying every field. This solves two distinct problems: construction that's expensive (heavy computation, a database round-trip, reflection-based setup) is paid once for the prototype and then avoided for every subsequent copy, and situations where the exact runtime type to construct isn't known to the caller — only a reference to an already-instantiated object of that type is available, so cloning it is the only way to get another one without a type-specific new call.
The mechanism that makes or breaks a Prototype implementation is how the copy happens. A shallow copy duplicates the object's own fields directly: primitive/value fields become independent, but any reference-type field (a nested object, a list, a map) is copied by reference — the original and the clone end up pointing at the same underlying nested object. Mutating that shared nested state through the clone silently mutates the original too, and vice versa. A deep copy recursively clones every referenced object as well, so the result is fully independent of the original with no shared mutable state anywhere in the object graph. Deep copy is safer but costs more (time, and sometimes correctness — cyclic object graphs need cycle-aware cloning logic to avoid infinite recursion) and is not always what shallow-clone-by-default language mechanisms actually give you.
Tradeoffs
| Approach | Benefit | Cost |
|---|---|---|
| Prototype (shallow clone) | Cheap — no deep traversal, fine when nested state is immutable or genuinely meant to be shared | Any shared mutable nested field creates action-at-a-distance bugs between the original and every clone |
| Prototype (deep clone) | Clone is fully independent — safe to mutate without affecting the original | More expensive; requires deliberate handling of cyclic references and every nested reference type |
| Constructor + manual configuration | No cloning machinery needed, explicit about every field set | Repeats expensive construction work every time; requires knowing the concrete type up front |
Copy constructor / static factory (Config.from(other)) |
Explicit, type-safe, easy to reason about in most OO languages | Still requires knowing the concrete type at the call site — doesn't help when only an interface reference is available |
| Serialize/deserialize round-trip (JSON, etc.) as a poor-man's deep clone | Trivially deep, no custom clone logic to maintain | Slow relative to direct cloning; silently drops anything the serializer can't represent (methods, non-serializable fields, certain types) |
When to use / when not to
- Use when constructing an object is expensive relative to copying one — object pools and game engines commonly spawn many similar entities by cloning a template rather than re-running full initialization for each one.
- Use when the caller only has an object reference, not the concrete class, and needs another instance shaped like it — common in plugin/extension systems where a registered "template" object is the only handle available.
- Use for building configuration variants off a common base (a base HTTP client config cloned and tweaked per environment) without redefining every field from scratch each time.
- Don't reach for it in typical CRUD/business-logic code where object construction is cheap and the concrete type is always known — a normal constructor call is simpler and avoids the shallow/deep-copy correctness trap entirely.
- Less relevant in languages with cheap, well-understood value/record types (immutable data classes, structs) — copying an immutable value is trivial and safe by construction, so Prototype's core justification (avoiding shared-mutable-state bugs) doesn't apply.
Common pitfall
Assuming a language's built-in "clone" mechanism does a deep copy when it actually does a shallow one. Java's Object.clone()/Cloneable is a field-by-field shallow copy by default (and is widely discouraged in modern Java in favor of copy constructors or static factories for exactly this reason); Python's copy.copy() is shallow (only copy.deepcopy() recurses); JavaScript's object spread ({...obj}) and Object.assign() are both shallow. Code that clones an object expecting full independence, then mutates a nested list or dict on the "clone," can silently corrupt the original prototype — a bug that's easy to miss in testing (if the test doesn't happen to mutate nested state) and painful to trace in production because the mutation appears to come from unrelated code touching what looks like a separate object.
Engineering Lens
The review question worth asking whenever a clone()/copy() method shows up in a diff is simple and often skipped: what happens to this object's reference-type fields on copy — are they meant to be shared, or does the caller expect independence? A clone method with no comment or test covering that distinction is a latent bug waiting for whichever field is added next without the implementer re-checking the copy logic. Prototype is a small pattern, but the shallow/deep decision it forces is exactly the kind of thing that's cheap to get right at definition time and expensive to debug once several call sites depend on the wrong assumption.
Related
- Builder Pattern — both produce a fully-configured object without a single monolithic constructor call, but Builder assembles incrementally from parts while Prototype copies a whole existing instance