Skip to content

Prototype

Creating an object by copying an existing one — and the shallow-copy bug that makes this the pattern most likely to cause a subtle data leak.

Intermediate17 minUpdated 8 August 2026

Prototype creates a new object by copying an existing one rather than constructing it from parameters. It is the least fashionable creational pattern and the one whose failure mode does the most damage, because a bad copy produces an object that looks correct and shares state with its original.

The pattern applies when an object is expensive to build, or when the configuration you want already exists as a live object rather than as a set of parameters.

The problem it addresses

A report template is assembled from a database read, a permissions check and a schema fetch — 200 ms of work. Every user who runs the report needs their own copy to modify.

// Rebuilds everything, per user, per run
const report = await buildReportTemplate(templateId);
report.filters.push({ field: 'region', value: user.region });

Constructing from parameters is not available: the parameters are no longer around, and reproducing them would mean re-reading the same three sources. What you have is a finished object that is nearly right.

const template = await buildReportTemplate(templateId);   // once, cached

// Per user
const report = template.clone();
report.filters.push({ field: 'region', value: user.region });

Shallow versus deep, and why it matters here more than anywhere

Conceptual diagramclone by fieldclone recursivelypush a filterpush a filterOriginaltitle, filters[Shallow copynew object, SAME filters arrayDeep copynew object, NEW filters arrayOriginal changes tooevery later copy is wrongOriginal unaffected
Diagram, as text
  • Original title, filters[ leads to Shallow copy new object, SAME filters array (clone by field)
  • Original title, filters[ leads to Deep copy new object, NEW filters array (clone recursively)
  • Shallow copy new object, SAME filters array leads to Original changes too every later copy is wrong (push a filter)
  • Deep copy new object, NEW filters array leads to Original unaffected (push a filter)

A shallow copy duplicates the object's own fields. Where a field is a reference, both objects now point at the same thing. Mutating the copy's filter list mutates the template's, and because the template is cached, every subsequent user inherits the previous user's filters.

This is a data-leak bug in the exact shape auditors care about: user A's region filter silently applied to user B's report. It passes every unit test that clones once.

Deep copying fixes it and brings its own problems:

  • Cost. Copying a large graph per request may be slower than rebuilding the parts you actually need.
  • Cycles. A naive recursive copy of a graph containing a back-reference never terminates.
  • Things that must not be copied. A database connection, an open file, a mutex, a logger. Copying a connection handle gives you two objects that think they own one resource.

Making the decision explicit

The honest implementation states its policy for each field:

class ReportTemplate {
  constructor(title, filters, schema, connection) {
    this.title = title;            // immutable string — share
    this.filters = filters;        // mutable array — must copy
    this.schema = schema;          // frozen object — share
    this.connection = connection;  // resource — share deliberately, never copy
  }

  clone() {
    return new ReportTemplate(
      this.title,
      this.filters.map((f) => ({ ...f })),   // copy the array AND its elements
      this.schema,
      this.connection,
    );
  }
}

Note filters.map(f => ({...f})) rather than [...this.filters]. Copying the array alone still shares every filter object inside it — the one-level-too-shallow bug, and the most common way this goes wrong after someone has already been told to copy the array.

Where the pattern actually shows up

Cached templates and configurations — as above: an expensive-to-assemble object that many callers need a private variant of.

Prototype registries — a map from a name to a fully configured exemplar, cloned on request. This is a factory whose products are objects rather than classes, and it is how plug-in systems let third parties register new types without touching your switch statement.

Undo and snapshots — copying an object to keep its previous value. This is close enough to Memento to be worth naming; the difference is that Memento deliberately hides the copy's internals from everyone but the original.

Test fixtures — clone a valid baseline object and mutate one field, rather than rebuilding a valid object per test. A test data builder usually does this better, because it makes the mutation explicit.

When not to use it

When construction is cheap. Calling the constructor is clearer than copying, and it cannot share state by accident.

When the object owns resources. Anything holding a connection, a file handle, a lock or a thread should not be cloneable, and the clone method should not exist rather than silently do the wrong thing.

When the graph is deep and mutable. The copy is expensive, the cycle handling is fiddly, and every new field is a chance to forget a case. Prefer making the graph immutable, or rebuilding.

Check your understanding

Common mistakes

Relying on the language's default copy. It is shallow, and the "deep" alternative usually means serialising, which drops unserialisable fields silently.

Copying the container but not its elements. [...list] is one level. The objects inside are still shared.

Cloning objects that own resources. Two owners of one connection, file or lock is an ambiguity with no good resolution.

Deep-copying a graph with cycles naively. Recursion without a visited set does not terminate.

Using Prototype where a constructor would do. If the parameters are available, construct. Copying can share state; construction cannot.

Forgetting a new field in clone(). Every field added later is a chance for the copy to silently diverge. Keep the copy next to the constructor so they are read together.

Practice

A cached `PricingRules` object is cloned per request and mutated with customer-specific overrides. Support reports that some customers occasionally see another customer's discount. Walk through how you would confirm the cause and fix it.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

The symptom is intermittent and cross-customer. That narrows the cause sharply before you read any code.

Show a strong answer

What the symptom already tells you. Cross-customer contamination that is intermittent means shared mutable state reached by concurrent or sequential requests. In a per-request-clone design there is exactly one candidate: the clone is not actually independent. That is worth stating before opening the file, because it stops the investigation from wandering into caching TTLs and serialisation.

Confirming it:

  • Read clone() and classify every field as immutable, copied, or shared-by-reference. The bug is a mutable field in the third group. Fields added after clone() was written are the likeliest candidates, since nobody updated the copy.
  • Reproduce deterministically rather than under load: clone the cached object twice, mutate the first, assert the second is unchanged, and assert the cached original is unchanged. That third assertion is the one usually missing, and it is the one that fails.
  • Check identity, not equality. assert(a.discounts !== b.discounts) is the test that catches this; comparing values passes right up until someone mutates.

The fix, in order:

  • Copy the mutable fields properly, elements included, not just containers.
  • Freeze what is handed out. If discounts is frozen after construction, an attempted mutation throws at the offending line instead of corrupting the cache silently — the bug becomes a stack trace pointing at its cause.
  • Add the identity assertions to the test suite, plus one that clones, mutates and then re-reads from the cache.

What I would change beyond the immediate fix: per-request mutation of a shared cached object is the risky part, not the clone. I would prefer the cached rules to be immutable, with customer overrides held as a separate small object and applied at read time. Then there is nothing to copy and no way to contaminate the cache — the class of bug is gone rather than this instance of it.

One thing I would check before closing: whether anything else clones this object, and whether the same pattern is used elsewhere in the codebase. A clone() that missed a field usually has siblings.

Design a plug-in system where third parties register new node types for a workflow editor. Compare a prototype registry (clone a registered exemplar) with a factory registry (call a registered creation function), and pick one.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Ask what the third party has to supply, and what happens when they get it wrong.

Show a strong answer

Prototype registry: the plug-in registers a fully configured instance; the editor clones it whenever the user drags a new node onto the canvas.

  • In its favour: the plug-in author supplies an example, which is easier to get right than a constructor, and the registered object can be inspected — the editor can read its ports, its default label and its icon to build the palette without instantiating anything.
  • Against it: correctness now depends on the plug-in's clone() being right. A third-party author who returns a shallow copy creates cross-node state sharing inside our editor, and the resulting bug report arrives at us. We cannot review their code and the failure is silent.

Factory registry: the plug-in registers a function that returns a fresh node.

  • In its favour: every call constructs from nothing, so there is no shared state to get wrong. The failure mode of a bad factory — throwing, or returning an invalid node — is loud and local.
  • Against it: the palette needs metadata that now cannot be read from an instance without creating one, so the plug-in must supply a separate descriptor. That is more API surface.

I would pick the factory registry, and the deciding argument is about who pays for a mistake. Prototype pushes a subtle, silent correctness requirement onto authors we do not control, and its failure looks like a bug in our editor. Factory pushes an explicit, loud requirement — return a new node — that is hard to get wrong and obvious when it is.

The design I would ship is a small hybrid: register { descriptor, create }, where the descriptor is a frozen plain object used to build the palette, and create is the factory. That recovers Prototype's real advantage — inspectable metadata without instantiation — without inheriting its copying hazard.

What would change my mind: if nodes were extremely expensive to construct and cheap to copy. Then the cost argument returns, and I would use prototypes internally while still exposing a factory to third parties, so the risky copying stays in code we own.

Key takeaways

  • Prototype creates by copying an existing object, for cases where construction is expensive or the parameters no longer exist.
  • Shallow copies share references, and when the original is cached that turns into cross-request data leakage — the pattern's signature failure.
  • Copying a container is not copying its contents. [...list] leaves every element shared.
  • Write clone() by hand, field by field, so each field is a decision. Language defaults are shallow and serialisation-based deep copies drop things silently.
  • Objects owning connections, files or locks should not be cloneable — two owners of one resource has no correct lifecycle.
  • Immutability dissolves the problem: if the parts cannot change, sharing them is safe.
  • Test with identity assertions, and always assert the original is unchanged.

Further reading

  • Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
  • Joshua Bloch, Effective Java, third edition (Addison-Wesley, 2018) — on the difficulties of clone and on defensive copying.
  • Brian Goetz and others, Java Concurrency in Practice (Addison-Wesley, 2006) — safe publication and immutability.
  • Martin Fowler, Patterns of Enterprise Application Architecture (Addison-Wesley, 2002).
  • Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004).

Check yourself

3 questions in this lesson. Answer each once to complete it.

No problem practices this lesson yet. Browse the practice problems

Next lessonSingleton and Its Alternatives
Lessons in this track