Skip to content

9.4.5 — Prototype

What the original Gang of Four book says: Decide what kinds of objects to create by keeping one example instance around, and make new objects by copying that example.

What that means when you are actually writing code: When getting an object into the right state is slow or fiddly, do it once and then stamp out copies, instead of re-deriving the same state from parameters every single time.

Prototype is the odd one out among the creational patterns. The others are about which class or how to assemble one. Prototype is about noticing that you already have a perfectly good object, and the cheapest way to get another is to copy it. That sounds simple, and the idea is. What is not simple — and what most of this chapter is about — is that copying an object correctly is genuinely hard, and getting the copy depth wrong produces one of the nastiest bug classes in software: two objects that look independent and are not.

First, a name collision to clear up

JavaScript's prototype — the object every value looks up properties on, covered in 3.6.4 — and the Prototype pattern share a word and an ancestor idea, but they are different things. The language feature is about delegation: child.__proto__ → parent, lookups walk up the chain, and the child keeps a live link to the parent. The pattern is about copying: you take a fully-configured instance and produce an independent duplicate with no ongoing link. This page is about the pattern. The overlap is real and worth knowing — Object.create(exemplar) is the delegation-flavoured implementation of the pattern, and section 5.5 covers exactly when that difference matters.

1. The story: the object that costs a second to build

A rules engine loads a tax configuration. It parses a 4 MB rules file, compiles 300 regular expressions, resolves a jurisdiction hierarchy, and validates the cross-references. Cold construction takes about 900 ms. Each incoming request needs its own engine instance, because it is going to mutate a few fields — the effective date, the currency, two feature toggles.

The obvious code:

typescript
function handle(req: Request) {
  const engine = new TaxEngine(loadRules(), req.date, req.currency, req.flags);  // ← 900 ms. Every request.
  return engine.compute(req.invoice);
}

There are three separate problems here, and they are worth separating, because different people notice different ones.

First, the cost is paid again and again for state that never changes. Ninety-five percent of the construction work — the parsing, the compiling, the validating — produces identical output every single time. Only three small fields actually differ from one request to the next.

Second, sharing one instance is not an option. The engine gets mutated per request, so a single shared instance is a data-corruption bug across concurrent requests. And in a clustered Node deployment it fails differently in each process (3.8.6), which is the worst kind of bug to reproduce.

Third, the constructor's parameters cannot express "like that one, but…" Some objects are far easier to describe by their difference than by their full specification. "The standard enterprise plan, but with SSO enabled" is one sentence; the same thing written out as constructor arguments is forty.

Prototype's answer is to build the expensive state once, keep it as an example, and serve each request a copy it can freely mutate.

typescript
const template = new TaxEngine(loadRules());     // ← paid once, at startup
function handle(req: Request) {
  const engine = template.clone();               // ← ~0.2 ms
  engine.configure(req.date, req.currency, req.flags);
  return engine.compute(req.invoice);
}

2. How you arrive at the pattern

Step 1 — Start naive. new Thing(params) at each use. This is correct when construction is cheap and every field is genuinely chosen fresh.

Step 2 — Wait for the force. One of three forces arrives, and each of them on its own justifies the pattern.

Construction is expensive relative to how often you need instances — parsing, compiling, I/O, heavy computation. The desired object is easier to describe as a difference from an existing one than as a full parameter list, like "this order, but with a different shipping address". Or the configured state cannot be reproduced from parameters at all — it was assembled interactively (a user's dashboard layout), accumulated at runtime (a warmed cache, a trained model), or received from elsewhere. There simply is no constructor that could rebuild it.

Step 3 — Draw the line between what varies and what stays fixed.

What variesa small number of fields, per instance
What stays fixedthe large, expensive, already-correct remainder

Notice this is a quantitative line, unlike the polymorphic lines of the previous pages — which is why Prototype feels different from its creational siblings. When the fixed part is by far the larger part, copying beats constructing.

Step 4 — Decide when the choice is made. At runtime, from an existing instance. The example itself is also chosen at runtime, which is why a prototype registry (section 5.4) is such a natural companion: registry.get("enterprise-plan").clone().

Step 5 — Name the pattern and say what it costs. The name is Prototype. Its cost is the entire second half of this page: copying is not a solved problem. A shallow copy silently shares mutable substructure. A deep copy is expensive, can loop forever on cycles, and cannot copy everything (sockets, file handles, functions, class identity through JSON). Choosing the copy depth per field is real design work, and getting it wrong produces the hardest bug class on this page: two objects that appear independent and are not.

3. The mental model

In one sentence: a prototype is a rubber stamp. You carve the design once, and then every impression is fast, identical, and independently yours to write on.

The analogy that makes it stick — the pre-filled form. A hospital keeps a pre-filled admission form with the hospital name, the department, the standard consent text, and the doctor's details already on it. Each new patient gets a photocopy and writes only their name and the date. Nobody re-types the boilerplate, and nobody edits the master. And here is the subtlety that maps exactly onto shallow-versus-deep copying: if the master form references a shared attachment ("see the standard terms, document 47"), then every copy points at the same document 47 — which is fine if that document never changes, and catastrophic if a patient scribbles on it.

When to reach for it. The signals are: "duplicate this and change one thing" · "save as template" · "clone the environment" · "spawn 500 of these enemies with slightly different positions" · "the same object but for next month". There is also the performance-flavoured version: "this constructor is slow and we call it in a loop."

The mental checkpoint before using it. Ask: can I express the difference more cheaply than the whole? If yes, Prototype. Then ask immediately: what inside this object is shared mutable state? That second question is not optional. It is where the pattern's bugs live, and answering it up front is what separates a working clone from a lurking one.

4. Structure

the exemplarbuilt once — expensiveparsed rules · compiledregexes · resolved refs.clone()copy Adate = Jan · USDcopy Bdate = Feb · EURcopy Cdate = Mar · INR① fast, independent copiesshared substructurethe shallow-copy trap② one mutation, three victimsdeep copy or freezedecide per field, deliberatelyThe pattern is one method; the design work is deciding, field by field, how deep the copy goes.
Figure 4 — Copies and the one thing that goes wrong. Blue: cheap independent instances, each mutated slightly. Red dashed: nested mutable objects that a shallow copy shares between all three — the defect that makes copies look independent while behaving as one. Green: the deliberate remedy, chosen per field rather than all at once.

The participants: the Prototype (declares clone()), the ConcretePrototype (implements the copy, including how deep it goes), the Client (asks a prototype to clone itself, never naming a class), and — in practice — a PrototypeRegistry that maps names to examples.

5. The implementation

5.1 The explicit clone(), with copy depth decided per field

typescript
interface Prototype<T> { clone(): T; }

class TaxEngine implements Prototype<TaxEngine> {
  constructor(
    private readonly rules: CompiledRules,     // (1) immutable + expensive → SHARE
    private effectiveDate: Date,               // (2) mutable value → COPY
    private flags: Set<string>,                // (3) mutable collection → COPY
    private auditLog: string[] = [],           // (4) per-instance history → RESET
  ) {}

  clone(): TaxEngine {
    return new TaxEngine(
      this.rules,                              // shared on purpose: frozen, 4 MB, never mutated
      new Date(this.effectiveDate),            // Date is mutable — a shared one is a live wire
      new Set(this.flags),                     // new Set, same string members (strings are immutable)
      [],                                      // deliberately NOT copied: a copy starts fresh
    );
  }
}

(1) Sharing is a decision, not a default. rules is shared because it is immutable and large — and that immutability has to be real (Object.freeze, readonly, or a class with no mutating methods), not merely intended. Document it right there in the field declaration, because otherwise the next person will "fix" the sharing.

(2) Date is mutable in JavaScript. new Date(this.effectiveDate) is not paranoia — copy.effectiveDate.setMonth(…) on a shared Date mutates the example and every sibling copy. The same is true for Map, Set, arrays, and any class instance with setters.

(3) Copy the container, but not necessarily the contents. new Set(this.flags) gives you an independent set whose members are shared, which is correct here because the members are strings and strings are immutable. If the members were objects, this line would be a shallow-copy bug in disguise.

(4) Some fields must be reset, not copied — and this is the field category people forget. Identity (id), audit trails, timestamps (createdAt), version counters, subscriber lists, open connections. A cloned entity that keeps the original's primary key is a database conflict. A cloned event emitter that keeps the original's listeners double-fires every handler. Write the "reset list" before you write clone().

What this does when you run it: template.clone() returns a TaxEngine that shares one immutable CompiledRules (no copy cost), owns its own Date, Set, and empty audit log, and can be mutated with no effect at all on the example or on any sibling copy.

5.2 The copy-depth decision table

This table is the page's most portable artifact — apply it field by field.

Field kindCopy strategyWhy
Primitives (string, number, boolean, bigint)assignimmutable by nature; "sharing" is meaningless
Frozen / immutable objectssharecopying wastes memory and time for zero safety gain
Mutable value objects (Date, Money with setters)copya shared mutable value is a wire between instances
Collections of primitivescopy the containernew Set(x), [...x] — the members need no copy
Collections of mutable objectsdeep copy or freeze the membersthe classic shallow-copy trap (Figure 4, red)
Identity fields (id, slug, createdAt)reseta duplicate identity is a data-integrity bug
Accumulated history (audit log, metrics, version)reseta copy has no past
Subscribers / listeners / callbacksreset, usuallyinherited listeners fire twice (3.8.5)
Live resources (sockets, file handles, pools, timers)share deliberately or re-openthese cannot be copied at all; decide explicitly
Back-references to a parent or ownerre-point to the new ownerotherwise the copy is still wired into the original's graph

5.3 structuredClone — the built-in deep copy, and its exact limits

Modern JavaScript ships a real deep-copy algorithm (3.6.11):

typescript
const copy = structuredClone(original);

What it handles that JSON.parse(JSON.stringify(x)) destroys — and this comparison is a frequent interview question, because the JSON round-trip is so widely used and so quietly lossy:

ValueJSON round-tripstructuredClone
Datebecomes a stringstays a Date
Map / Setbecomes {} — silently emptypreserved
undefined propertydroppedpreserved
NaN, Infinitybecome nullpreserved
BigIntthrowspreserved
Circular referencethrowshandled correctly
ArrayBuffer / typed arraysmangled into objectspreserved
Class instancebecomes a plain object — methods gonealso a plain object — prototype not preserved
Function / Symbol / DOM nodedropped (or throws)throws DataCloneError

The two limits to remember: structuredClone does not preserve class prototypes (your TaxEngine comes back as a plain object with no methods), and it throws on functions. So it is excellent for plain data — configuration trees, message payloads, editor documents — and wrong for objects with behaviour. For those, write clone() explicitly, which is what section 5.1 does and why the explicit form still earns its place.

5.4 The prototype registry

Prototype's natural partner is a named collection of examples, so callers say what kind rather than how to build.

typescript
class ShapeRegistry {
  #protos = new Map<string, Prototype<Shape>>();
  register(name: string, p: Prototype<Shape>) { this.#protos.set(name, Object.freeze(p)); }
  create(name: string): Shape {
    const p = this.#protos.get(name);
    if (!p) throw new UnknownShapeError(name);
    return p.clone();                    // ← the registry never exposes the exemplar itself
  }
}

registry.register("enemy.grunt", gruntTemplate);      // loaded from data at startup
registry.register("enemy.boss",  bossTemplate);
const e = registry.create("enemy.grunt");             // fresh, mutable, cheap

The highlighted line is the whole safety property: callers receive copies only, never the example, so no caller can corrupt the template for everyone else. Freezing the registered prototype adds a second line of defence. Compare this with 9.4.2's registry — the same lookup shape, but the values are configured instances rather than constructor functions, which is exactly the case where the configuration came from data rather than from code.

5.5 Object.create — the delegation flavour

typescript
const base = Object.freeze({ retries: 3, timeoutMs: 5_000, region: "ap-south-1" });
const cfg  = Object.create(base);        // cfg delegates to base; it does NOT copy
cfg.timeoutMs = 30_000;                  // an own property shadows the inherited one

cfg.timeoutMs;   // → 30000  (own)
cfg.retries;     // → 3      (inherited — read through the prototype chain)
Object.keys(cfg) // → ["timeoutMs"]  ← only own properties; inherited ones are invisible here

This is Prototype implemented with delegation instead of copying, and the difference is in how it behaves, not just in how it looks: change base.retries later and every delegating object sees the new value immediately. That is a feature (live defaults, cheap memory) and a hazard (spooky action at a distance), plus the surprise that iteration and spreading see only own properties. Use it for read-mostly default layers; use real copying whenever the derived object will be mutated, serialized, or inspected with Object.keys.

5.6 Python

python
import copy
from dataclasses import dataclass, field, replace

@dataclass
class TaxEngine:
    rules: CompiledRules                                   # shared, immutable
    effective_date: date
    flags: set[str] = field(default_factory=set)
    audit: list[str] = field(default_factory=list)

    def clone(self) -> "TaxEngine":
        return TaxEngine(self.rules, self.effective_date, set(self.flags), [])   # explicit

engine2 = copy.copy(engine)       # shallow: nested objects SHARED
engine3 = copy.deepcopy(engine)   # deep: handles cycles via a memo dict; customize with __deepcopy__

Python names the two depths right there in the standard library, which makes the choice unavoidable and explicit — a small language-design lesson. copy.deepcopy also copies what you may not want copied (the rules, an open connection), so real classes override __deepcopy__ or, better, expose an intention-revealing clone() as above.

6. Five domains, the same shape

(a) Document and entity templates. "Save as template" in any editor, invoice templates, email campaign templates, Jira issue templates. The example was configured interactively by a human, so there is no constructor that could reproduce it — which is force number three from section 2 in its purest form.

(b) Game and simulation entities. Spawning 500 enemies: load one configured Grunt from data, then clone() per spawn with a new position. Constructing each one from a data file 500 times is the naive cost; a shared immutable mesh and texture handle plus a per-instance transform is the copy-depth decision, and it maps exactly onto section 5.2's table.

(c) Expensive-to-initialize objects. Compiled schemas (Ajv validators), parsed grammars, warmed caches, loaded ML models. Often the right split is: share the immutable compiled artifact, copy the small mutable wrapper — the TaxEngine shape from section 5.1.

(d) Immutable updates — Prototype without the name. Every one of these is "copy the example, change a field":

typescript
const next = { ...state, status: "shipped" };          // Redux reducer
const arr2 = [...arr, item];                           // append without mutating
const d2 = new Date(d); d2.setDate(d.getDate() + 1);   // date arithmetic done safely

Recognising spread-updates as Prototype is genuinely useful, because it means the shallow-copy trap (section 5.2) applies to your reducers too. { ...state } copies one level, so state.user.address is still shared, which is the single most common source of "why did my Redux state mutate?" bugs.

(e) Infrastructure, where the pattern is the whole product. A Docker image is the example, and a running container is a copy of it (2.9). Restoring a database from a snapshot is the same move. So is creating a Git branch. All of these are this pattern at the scale of whole machines rather than single objects. Copy-on-write (page tables in 2.5, filesystem snapshots in 2.6) is the same pattern with the copy deferred until the first mutation — the optimization that makes fork() and container startup fast, and a great answer to "where does this pattern appear outside application code?"

7. Variants

VariantShapeUse when
Explicit clone()a class method deciding depth per fieldobjects with behaviour and mixed field kinds — the default
Copy constructornew Order(existing, { status: "shipped" })languages or teams that prefer constructors; same semantics
structuredClonebuilt-in deep copyplain data; no class methods, no functions
Spread / replace{ ...obj, field }shallow immutable update; know that it is shallow
Object.createdelegation, not copyingread-mostly default layers; live inheritance wanted
Prototype registryname → example → clonekinds defined by data or configuration rather than by classes
Copy-on-writeshare until the first write, then copycopies are frequent and mutations are rare

On copy-on-write, briefly, because it is the most useful advanced variant. Wrap the shared state and copy it lazily:

typescript
class CowDocument {
  #shared: Readonly<Doc>; #own?: Doc;
  constructor(shared: Readonly<Doc>) { this.#shared = shared; }
  read(): Readonly<Doc> { return this.#own ?? this.#shared; }
  mutate(fn: (d: Doc) => void) {
    this.#own ??= structuredClone(this.#shared) as Doc;   // ← the copy happens here, once
    fn(this.#own);
  }
}

Ten thousand read-only "copies" cost nothing; the few that are written pay for themselves. This is exactly the operating-system trick, and naming that lineage in an interview lands well.

8. Where you already use it

In the wildNotes
Object.create(proto), Object.assign({}, x), { ...x }the language's own copy and delegate operations
structuredClone, Array.prototype.slice, new Map(m)built-in copies of varying depth
React { ...props }, Redux reducers, Immer's produceimmutable updates are copy-with-changes
Object.freeze + Object.create config layeringlive defaults with local overrides
Docker images → containers; K8s PodTemplate → podsprototypes at infrastructure scale
git branch, database snapshot restore, VM templatescopy-on-write prototypes
fork() and copy-on-write pages (2.5)the operating system's version

9. Ways to get it wrong

  1. The shallow-copy trap. { ...obj } copies one level; obj.nested is shared. Copies look independent and are not, and the symptom appears far from the cause.

    The fix: the section 5.2 table, applied per field; freeze what you share.

  2. JSON round-trip cloning. JSON.parse(JSON.stringify(x)) loses Date, Map, Set, undefined, BigInt (throws), and class identity. It is still extremely common in production code.

    The fix: structuredClone for data, an explicit clone() for behaviour.

  3. Cloning identity. A copied entity that keeps the original's id collides on insert or, worse, overwrites the original.

    The fix: the reset list (section 5.1 note 4), applied deliberately.

  4. Cloning live resources. Sockets, file handles, database pools, timers, AbortControllers — not copyable; a "clone" holding the original's socket means two owners for one resource and a double-close.

    The fix: share deliberately, or re-acquire in the clone, and never let a generic deep-copy walk into these.

  5. Cloning subscribers. A cloned emitter carrying the original's listeners fires every handler twice — a bug that shows up as duplicate emails, not as an exception.

  6. Deep-copy cost mistaken for free. structuredClone of a large graph on a hot path is a serialization-scale cost on the main thread (3.8.1).

    The fix: measure; consider copy-on-write.

  7. A mutable example. If any caller can reach the template, one accidental mutation poisons every future clone.

    The fix: freeze the example and hand out copies only (section 5.4).

  8. Prototype where a factory belongs. If every field is chosen fresh and nothing is expensive, copying an example is a confusing way to spell new Thing(params).

  9. Object.create surprises. Inherited properties are invisible to Object.keys, JSON.stringify, and spread — so a delegating object serializes as nearly empty, which is a genuinely baffling bug the first time you hit it.

10. Prototype compared with its neighbours

Compared withThe differenceChoose Prototype when
Factory Methoda factory builds from parameters; Prototype copies an instancethe state is expensive or cannot be reproduced from parameters
Buildera Builder assembles from parts; Prototype starts from a finished wholeyou have a good starting object and want a delta
Abstract Factoryfamilies of new objects versus copies of onethere is an example, not a family constraint
SingletonSingleton shares one instance; Prototype hands out many independent onesinstances are mutated per use — the direct opposite decision
Memento (9.4.1 section 2)saves state to put back lateryou want a live object now, not a saved point
Flyweight (9.4.1 section 2)shares one copy between many objectseach copy must be changeable on its own

The sharpest contrast is Singleton. Both start from "one configured instance exists". Singleton concludes everyone uses that one; Prototype concludes everyone gets their own copy of it. The deciding question is a single one: is the instance mutated per use? Mutated means Prototype (or a factory). Read-only means sharing is cheaper and simpler. Getting this backwards produces the two classic bugs in symmetry: shared mutable state (Singleton misused) and pointless copying of immutable state (Prototype misused).

11. Interview calibration

The 45-second answer, in the order you would say it:

Prototype creates new objects by copying a configured instance instead of building from parameters. It earns its place when construction is expensive, when the object is easier to describe as a delta from an existing one, or when the state can't be rebuilt from parameters at all — a template a user configured interactively. In JavaScript it's usually a clone() method or structuredClone, and the real design work is copy depth: I decide per field whether to share (frozen, expensive), copy (mutable values and collections), or reset (ids, audit logs, listeners, live handles).

The classic bug is a shallow copy that shares nested mutable state, so copies look independent and aren't — and JSON.parse(JSON.stringify(x)) additionally destroys Dates, Maps, Sets, and class identity. It also shows up as infrastructure: a container is a clone of an image, and copy-on-write is this pattern with the copy deferred.

Follow-up questions, with the seed of each answer:

  • "Is JavaScript's prototype chain this pattern?" — Related but different: delegation with a live link versus copying with independence. Object.create is the delegation-flavoured implementation; use it for read-mostly defaults.
  • "How do you deep-copy safely?"structuredClone handles cycles, Date, Map/Set, and typed arrays; it does not preserve class prototypes and throws on functions. Beyond that, an explicit clone().
  • "What must never be copied?" — Identity fields, accumulated history, subscribers, and live resources. Name the reset list.
  • "When is copy-on-write worth it?" — Many logical copies, few mutations; it is the OS fork() argument, one level up.
  • "Prototype or Factory here?" — If every field is chosen fresh and construction is cheap, a factory. If the interesting state already exists somewhere, a prototype.

Recall

  • Prototype = create by copying a configured example. Three forces justify it: construction is expensive (parse, compile, I/O), the target is easier described as a delta ("that, but for next month"), or the state is unreproducible from parameters (a user-configured template, a warmed cache, a trained model).
  • The design work is copy depth, decided per field: share frozen or expensive state (assert it with toBe in tests), copy mutable values (Date, Map, Set, arrays — Date is mutable, and this bites people), reset identity, audit logs, timestamps, subscribers, and live handles. Write the reset list before you write clone().
  • The trap: a shallow copy shares nested mutable substructure, so copies look independent and behave as one — including { ...state } in reducers. JSON.parse(JSON.stringify(x)) additionally destroys Date, Map/Set, and undefined, turns NaN into null, throws on BigInt and cycles, and flattens class instances. structuredClone fixes all of that except class prototypes, and throws on functions.
  • Variants: explicit clone() (objects with behaviour) · copy constructor · structuredClone (plain data) · spread (shallow, know it) · Object.create (delegation with a live link — inherited props are invisible to Object.keys, spread, and JSON.stringify) · prototype registry (name → example → clone; hand out copies only, freeze the example) · copy-on-write (share until the first write — fork(), container startup, filesystem snapshots).
  • Versus Singleton, the deciding question: is the instance mutated per use? Mutated means copies; read-only means share. At systems scale the pattern is the product: Docker image → container, VM template, PodTemplate, snapshot restore, git branch.

Self-test: Give three independent reasons to copy instead of construct. Which field kinds must be reset rather than copied, and why each? Name five things the JSON round-trip destroys and the two things structuredClone still cannot do. What is the difference between Object.create(x) and copying x? What single question decides Prototype versus Singleton?

Quiz Bank

FoundationalGive the three forces that justify Prototype, with an example of each, and say what the pattern costs.

Force one — construction is expensive relative to how often you need an instance. A tax engine that parses a 4 MB rules file and compiles 300 regular expressions takes about 900 ms; requests need their own mutable instance. Building the immutable part once and cloning per request turns 900 ms into microseconds — and note the shape of it: ninety-five percent of the work produces identical output every time, which is the quantitative varies/fixed line this pattern draws.

Force two — the target is easier described as a delta. "This order, but shipping to the office address" is one sentence; the equivalent constructor call restates thirty fields, and every restatement is a chance to get one wrong.

Force three — the state cannot be reproduced from parameters at all. A dashboard layout a user arranged by dragging, a cache warmed by traffic, a model fine-tuned at runtime: no constructor could rebuild it, so copying is the only way to get a second one. This third force is the one that makes Prototype irreplaceable rather than merely convenient.

The cost: copying is not a solved problem. A shallow copy silently shares nested mutable state, so copies appear independent and are not — with symptoms appearing far from the cause. A deep copy is expensive, can loop on cycles, cannot copy functions or live handles, and (through structuredClone) loses class prototypes. There is also a maintenance cost: clone() must be updated whenever a field is added, and a forgotten field is a silent sharing bug — which is exactly why the property test in section 9 exists.

FoundationalCompare JSON round-trip cloning, structuredClone, and an explicit clone method. When is each correct?

JSON.parse(JSON.stringify(x)) is the most-used and least-correct option. It destroys Date (becomes a string), Map and Set (become {} — silently empty, the nastiest of the failures because nothing errors), undefined properties (dropped), NaN and Infinity (become null), and class identity (methods gone); it throws on BigInt and on circular references. It is acceptable only for data you already know to be JSON-shaped — typically something that arrived as JSON in the first place.

structuredClone(x) implements the HTML structured-clone algorithm (3.6.11): it handles cycles, preserves Date, Map, Set, RegExp, ArrayBuffer, and typed arrays, and keeps undefined. Its two limits are firm: it does not preserve class prototypes (a TaxEngine returns as a plain object with no methods) and it throws DataCloneError on functions, symbols, and DOM nodes. So it is the right choice for plain data — configuration trees, editor documents, message payloads, worker transfers.

An explicit clone() is the right choice for objects with behaviour or mixed field kinds, because it is the only option that lets you decide per field whether to share (frozen expensive state), copy (mutable values), or reset (ids, history, listeners, live handles) — and those decisions are the actual design content of the pattern (section 5.2). It is also the only one that can be documented and tested per field.

Rule of thumb: data means structuredClone; objects mean clone(); the JSON round-trip only when the value is genuinely JSON.

AppliedA team clones domain entities to implement a duplicate-record feature and reports three bugs: duplicates overwrite originals on save, emails go out twice, and edits to a duplicate change the original's line items. Diagnose each and give the fix.

All three are the same root cause — an under-specified copy — appearing in three different field categories, which makes it an ideal illustration of section 5.2.

Bug one: duplicates overwrite originals on save. The clone kept the original's primary key, so the ORM issued an UPDATE rather than an INSERT. Category: identity fields, which must be reset, not copied — id, slug, external references, createdAt, version.

The fix: reset them in clone() and, to prevent recurrence structurally, make the entity's constructor require a fresh id so a clone cannot be built without providing one.

Bug two: emails go out twice. The cloned aggregate carried the original's registered listeners or pending domain events, so both objects fired the same handler (3.8.5). Category: subscribers and accumulated history — reset.

The fix: start the clone with an empty listener list and an empty pending-event queue; a copy has no past and no audience.

Bug three: editing the duplicate changes the original's line items. The copy was shallow — { ...order } or a new Order(order) that copied the array reference — so both orders point at one array of mutable LineItem objects. Category: collections of mutable objects, which need deep copy or frozen members.

The fix: items: order.items.map(i => i.clone()), or make LineItem immutable so sharing is safe.

The systemic fix, beyond the three patches: write the copy-depth table for the aggregate and encode it as tests — an independence test that mutates every field of the copy and asserts the original is unchanged, plus an identity test asserting id differs, plus deliberate toBe assertions for the fields you intend to share. Then add the property test that walks generated mutation paths, because the failure mode here is always "the one field nobody thought about", and only a generated test finds that reliably.

The reframing for the team: cloning an entity is not a technical operation, it is a domain decision. "What does it mean to duplicate an order?" has a business answer — new identity, no history, same catalog references, fresh audit trail — and clone() should read as that answer rather than as a memory operation.

InterviewDistinguish the Prototype pattern from JavaScript's prototype chain, and say when Object.create is the right implementation.

They share a word and an intuition — "start from an existing object" — and they differ in the mechanism, which changes the semantics completely.

JavaScript's prototype chain is delegation with a live link. Object.create(base) produces an object that has no copy of base's properties; reads that miss walk up the chain to base (3.6.4). Change base.retries afterwards and every delegating object sees the new value instantly. Writes, by contrast, create an own property that shadows the inherited one, so mutation is local while reading is shared.

The Prototype pattern is copying with independence. clone() produces an object with its own properties, and later changes to the example are invisible to it.

When Object.create is the right implementation: read-mostly layered defaults — a configuration base with per-environment overlays, a shared method bag, a large frozen defaults object where copying per instance would waste memory. You get O(1) creation, live updates to the defaults, and minimal memory.

When it is wrong, and the specific surprises: inherited properties are invisible to Object.keys, to for…in with hasOwnProperty guards, to object spread, and to JSON.stringify — so a delegating configuration object serializes as nearly empty, and a spread { ...cfg } silently loses every inherited default. That single behaviour has produced a great deal of confused debugging. Also, deep property mutation through the chain (cfg.nested.x = 1 when nested is inherited) mutates the shared object, which is the delegation flavour of the shallow-copy trap.

The rule: delegate for read-mostly defaults you want to stay live; copy whenever the derived object will be mutated deeply, serialized, iterated, or handed to code that does not know about the chain.

StaffYou are designing a collaborative document editor. Users duplicate documents, create templates from documents, and the system snapshots documents for version history and undo. All three sound like copying. Design the copy strategy, decide where Prototype ends and other patterns begin, and address performance at 200-page documents.

Start by pulling apart three jobs that all look like copying and are not. Treating them as one job is the single most expensive mistake available here, so it is worth ten minutes at a whiteboard before any code.

Duplicating a document gives you a new, live document with its own identity. Somebody will open it and edit it. That is Prototype, exactly as this chapter describes it.

Creating a template gives you a new live document too, but one that has deliberately had things taken out of it: the text and the data specific to the original are removed, while the structure and the styling stay. That is Prototype plus a stripping step. The stripping step is a business decision about what belongs in a template, not a detail of how copying works, so it belongs in your domain code and not inside clone().

Snapshotting for history and undo gives you something quite different: a dead record whose only purpose is to be restored later. Nobody ever runs it or edits it. That is the Memento idea from 9.4.1 section 2, and its requirements have almost nothing in common with the other two. A snapshot needs to be small, needs to survive being written to storage and read back, needs a version number so that a snapshot saved last year still loads after the document format changes, and must never accidentally turn into something editable.

Build all three on one clone() method and you will eventually get the two bugs this always produces: snapshots that are still holding live event listeners, and templates that quietly kept a reference to the original document's collaborators.

Copy strategy per operation. Duplicate: deep-copy the content tree; reset the id, the permissions (the copy inherits the creator's ownership, not the original's ACL — a security decision, and the kind of thing that must be explicit), the comment threads, and presence; share immutable assets by reference (images, fonts) with reference counting rather than duplicating megabytes of blobs. Template: the same copy, then apply the redaction rules — and make those rules data, so product can change what a template keeps without a deploy. Snapshot: not a clone at all; serialize to a compact, schema-versioned representation, and store deltas plus periodic keyframes rather than full copies.

Performance at 200 pages is the crux, and copy-on-write is the answer. A 200-page document is a large tree; a full deep copy per duplication is a multi-hundred-millisecond main-thread stall (3.8.1) and a memory spike. Model the document as an immutable persistent tree with structural sharing: a "copy" is a new root pointing at the same children (O(1)), and an edit path copies only the nodes from the edited leaf up to the root (O(depth), typically a handful of nodes). This gives you duplication for free, snapshots for free (a snapshot is a retained root), and undo for free (undo is re-pointing at a previous root) — one data-structure decision that dissolves all three problems, which is exactly why it is worth the upfront cost. It is also the same copy-on-write idea as OS page tables (2.5), which is a useful lineage to name.

Consequences to accept openly: structural sharing means retained old roots keep memory alive, so history needs an eviction policy (keep N recent roots plus periodic keyframes, drop the rest); and every mutation must go through the persistent-update API, because one in-place mutation of a shared node corrupts every version that shares it — so the node type must be genuinely immutable (frozen in development, enforced by types in production) rather than immutable by convention.

The one line to put in the design document: Prototype handles duplication and templates, snapshots handle history, and copy-on-write is how both of them are implemented underneath.

Then the rule that keeps it honest: a snapshot is data, and a duplicate is a document. That sounds like hair-splitting until the day somebody restores a snapshot straight into a live editing session, at which point it is the only thing standing between you and a corrupted document.

Flashcards

FlashPrototype in one line

Create by copying a configured example instead of building from parameters. Three forces: expensive construction, delta is easier than the whole, state unreproducible from parameters.

FlashCopy depth per field

Share frozen/expensive · copy mutable values and containers (Date is mutable!) · reset id, history, timestamps, listeners, live handles · re-point back-references.

FlashJSON round-trip destroys

Date→string, Map/Set→{}, undefined dropped, NaN/Infinity→null, BigInt throws, cycles throw, class instance→plain object.

FlashstructuredClone limits

Handles cycles, Date, Map, Set, typed arrays. Does NOT preserve class prototypes; throws DataCloneError on functions, symbols, DOM nodes.

FlashObject.create vs clone

Delegation with a live link vs copy with independence. Inherited props are invisible to Object.keys, spread, and JSON.stringify — the classic surprise.

FlashPrototype vs Singleton

Same start, opposite conclusion. One question decides it: is the instance mutated per use? Mutated → copies. Read-only → share.

Scenario Drill

DrillA SaaS platform lets customers create pipeline templates: a template is an example pipeline with 40 configured steps, credentials, schedules and notification rules. Customers instantiate templates hundreds of times per day, edit the instances, and expect edits never to affect the template — but support keeps receiving tickets that editing an instance changed the template, or that instances stopped working when a template was deleted. Design the fix end to end.

Diagnose from the two symptoms, because together they pin the defect exactly. "Editing an instance changed the template" is the shallow-copy trap: instantiation copied the pipeline object one level, so nested structures — the step array's elements, the notification-rule objects, the schedule — stayed as shared references, and editing a step mutated the object the template also pointed at. "Instances stopped working when a template was deleted" is the same defect from the other side: instances hold references into the template's graph (credentials by reference, step definitions by pointer), so deleting the template cascaded into live instances. Both say one thing: instantiation was implemented as reference-sharing while being documented as copying.

The fix, in layers. First, define copy semantics per field as a written table, and treat it as domain design rather than a technical detail. Step definitions, schedules, and notification rules: deep copy — they are exactly what customers edit. Credentials: neither copy nor share — instances should hold a credential reference by id resolved at run time through the secret store, because copying secrets multiplies the blast radius of a leak and sharing an object graph creates the deletion coupling; this is the field where the naive answer is wrong in both directions. Template identity and lineage: reset the id, but record createdFromTemplateId and templateVersion — customers will ask "which template did this come from, and has it changed since?", and that question is unanswerable later if you do not capture it now. Run history, execution logs, cursors, and enable/disable state: reset.

Second, make the template immutable and versioned. Freeze published templates; editing produces version N+1 rather than mutating version N. This alone removes an entire class of tickets, because a template that cannot be mutated cannot be corrupted by an instance — and it gives the deletion problem a clean answer: templates are never hard-deleted, they are archived, and instances reference an immutable version that will always exist.

Third, serve instantiation through a registry that hands out copies only (section 5.4): callers ask templates.instantiate(id, version) and never receive the example itself, so no code path exists that could alias it.

Fourth, address the performance question honestly. Forty steps deep-copied hundreds of times per day is trivial — measure before optimizing; if template sizes grow to thousands of steps, move to structural sharing with copy-on-write rather than pre-optimizing now.

Fifth, prove it with tests that would have caught both tickets: an independence test that mutates every field of an instance and asserts the template version is byte-identical; a deletion test that archives a template and asserts every derived instance still executes; and a property test that walks generated mutation paths, since the recurring failure mode is the one field nobody enumerated.

Sixth, ship the migration: existing instances created by the broken code already share structure, so a one-time backfill must deep-copy their shared substructures and re-point the credential references — and it must run before the immutability change, or frozen templates will start throwing where the old code silently mutated.

The sentence for the design review: instantiating a template is a domain operation with a domain answer — new identity, no history, its own editable structure, referenced secrets, recorded lineage — and the bugs happened because it was implemented as a memory operation instead.