01 / The idea
The configuration you need may already be in an object.
A dashboard starts with one incident-count card. Constructing it from a title and a few options is clear. A factory that knows the defaults would work too.
Then users save configured cards and duplicate ones they have already edited. The source may have a 15-minute window, a disabled filter, or a combination that no named preset describes. Rebuilding the new card from the original defaults would lose those choices. Copying fields in every menu handler would make each handler responsible for the card’s internal state.
Prototype creates new objects by copying an existing instance through an explicit duplication operation. The caller selects the instance to start from. That instance’s implementation owns the copy rule, so the caller does not have to reconstruct its configuration.
The existing object is playing the role of a prototype; it does not need to belong to a special “template” class. A card you edited a moment ago can be the source for the next card.
02 / See the shape
The useful question is what “copy” promises.
The Basic form keeps the source title and accepts a new card ID. It contains only strings, so there is no nested editable state to separate. The caller asks an existing instance for a duplicate.
The In the wild card owns a query containing a window object and an array of filter records. It also refers to a read-only data definition and may hold the result of a previous preview. The duplication rule now needs to account for all four responsibilities.
| Part of the card | What the duplicate receives | Why |
|---|---|---|
| Title and query settings | Current values, with independent editable query storage | Later edits belong to one card. |
| Data definition | The same read-only definition | Sharing is intentional; the card cannot edit it. |
| Card ID | A different ID supplied by the caller | This product action creates another entity. |
| Last preview | No preview yet | A previous result belongs to the source’s runtime state. |
An existing card creates another card with its title and a caller-supplied new ID.
export class BasicCard {
readonly id: string;
readonly title: string;
constructor(id: string, title: string) {
this.id = id;
this.title = title;
}
duplicate(newID: string): BasicCard {
checkID(this.id, newID);
return new BasicCard(newID, this.title);
}
} type BasicCard struct{ ID, Title string }
func (c BasicCard) Duplicate(newID string) (BasicCard, error) {
if err := checkID(c.ID, newID); err != nil {
return BasicCard{}, err
}
return BasicCard{newID, c.Title}, nil
} At the call site, A is created from the preset and edited to a 30-minute latency query. B is then created from A. B starts with A’s current values, while the preset remains at 15 minutes. That is the creation decision the pattern makes visible.
The shared contract and its boundariesIdentity, validation, and ownership
A duplicate must have a different ID from its immediate source. IDs and filter codes use 1–24 lowercase ASCII letters, digits, or hyphens and start with a letter. The caller owns uniqueness across the wider dashboard; the method cannot discover every existing card.
Query edits require an integer window from 1 to 240 minutes, then a valid first-filter code. Both checks happen before any mutation. A rejected edit preserves every value and the previous preview. A successful edit preserves the remaining filters and clears only the edited card’s preview.
The source and both copies can outlive each other. Views copy editable query data so inspecting a card does not expose another mutation path. The shared definition contains only a name and version and has no editing operation. These are serialized in-memory models; persistence, live queries, authorization, and concurrent edits are outside their contract.
Reading the TypeScriptA new outer object is only the beginning
Runtime-private #query holds the mutable state. The copy helper creates a new
query object, a new window, a new filter array, and a new record for each filter. Those records
currently contain only a string and a boolean. The data definition is a frozen object of primitive
values, so retaining that reference is deliberate.
The basic form’s readonly fields are type-checker restrictions, not runtime freezing.
The practical card exposes edits through methods.
Reading the GoStruct values, slice storage, and package ownership
The window struct is copied by value. The filter slice needs a new backing array; copying only its slice header would keep shared storage. Each filter here contains value fields, so copying the elements is sufficient for this particular type.
The definition pointer stays shared. Its fields are unexported and it has no public mutator; the owning package must preserve that read-only contract. Go does not freeze the allocation. Preview inspection copies its optional integer so a caller cannot change the original through the returned pointer. Duplication and edits return errors for the caller to handle.
Reading the PythonDataclasses and explicit list copies
The Python card keeps the editable query in ordinary dataclasses and copies the window, filter objects, and filter list explicitly. The frozen definition protects its primitive fields, but that is a small model boundary rather than a general deep freeze. The duplicate operation shares that definition and resets the preview.
03 / Follow the values
Two cards can still point to one editable query.
Watch the intended copy policy, or step through its outcomes. In Try it, choose the shallow
rule and create A and B. Select A, change its window to 30 and its first filter to latency, then apply. The outer cards have different IDs, but all three display the edit because
their query is shared.
Switch to the Prototype copy contract, recreate the copies, and repeat the edit. The query labels now differ. Only A changes; the data definition stays shared. Finally, reset, make A, edit it, and choose “Duplicate A into B.” Predict B’s starting values before creating it.
Copy the configured card.
Source preset-api: query Q1, 15 minutes; http-5xx enabled, timeout enabled; preview 42. Copy A does not exist. Copy B does not exist. All existing cards share the same read-only definition.
API incidents · configured cards
Source
preset-api
Query Q1
15 minhttp-5xxenabledtimeoutenabled
Preview 42
Copy A
No card yet.
Duplicate to create A.
Copy B
No card yet.
Duplicate to create B.
Incident feed · v1
One read-only definition, shared by every card.
Equal Q labels mean the same editable query.
Start with a configured card.
The source already has a 15-minute query and a sample preview.
Reduced motion: choose a scene to see its completed state.
Read this scene
The source already has a 15-minute query and a sample preview.
Source preset-api: query Q1, 15 minutes; http-5xx enabled, timeout enabled; preview 42. Copy A does not exist. Copy B does not exist. All existing cards share the same read-only definition.
The deliberate shallow-copy mistakeTypeScript comparison only
shallowDuplicateForComparison(newID: string): IncidentCard {
checkID(this.#id, newID);
return new IncidentCard(newID, this.#title, this.#query, this.#definition, null);
// Intentionally wrong for independent editing: only the outer card is new.
} Both versions create a new outer card, assign a new ID, and clear its preview. The difference is whether the query remains a reference to the source’s query. This helper shows that one missing ownership decision can defeat the intended independence.
Editing A clears A’s preview, but cannot clear the source’s preview if A has silently changed the source’s query. In the shallow run the source can therefore show changed settings beside its old sample result. Copy boundaries can also become cache-invalidation boundaries.
The exact mistake differs by representation. A shallow Go struct assignment shares the filter backing array but copies the value-only window struct. Adding a shared mutable smart pointer would change the copy contract.
04 / Try a decision
Choose the contract before the traversal.
The same object can be copied for a different purpose: a backup snapshot, another editable entity, or a view that intentionally shares a model. Those tasks need not use the same identity and ownership rules.
Reason through the alternatives
Recursively copy every reachable value and keep every field unchanged. A generic traversal does not know that this action creates a new card identity or that the previous preview should be cleared. It also duplicates a definition we intentionally treat as read-only shared data. Such a copy can fit a plain data snapshot, but it does not establish this product’s duplication contract.
Create a new card object, then reuse all of the source’s fields. A new outer object is not enough. Its editable query can still be the source’s query, so changing one filter changes both cards. Reusing the source ID or preview also carries state that belongs to the original card.
Copy the editable query, keep the read-only definition, supply a new card ID, and clear the preview. This follows the stated product behavior: each card can be edited independently, all can use the same read-only definition, and the new card has its own identity and no stale preview. If the definition becomes editable, this sharing decision must be revisited.
A shallow copy is sufficient when the shared interior is intentionally immutable or shared editing is the desired behavior. A deep data snapshot is useful when its supported types and identity policy match the task. The choice follows the contract.
What does the new instance own? What may it share? Which identity or runtime fields change, and which future field would make you revisit the implementation?
This note stays on this page and is not saved.05 / Give it a real job
The menu chooses the source. The card owns duplication.
In an editor, a palette holds configured card instances loaded from saved templates or assembled by application code. The Duplicate action selects one, obtains a new card ID, calls its duplication operation, and inserts the result into the dashboard. The query runner executes the new card later; cloning does not make a network request.
Our implementation has one concrete card type. The classic pattern can put a duplication contract in front of several concrete types so a client can copy a selected instance without knowing its constructor. A heterogeneous palette may justify that interface. One concrete type does not need a class hierarchy just to express the creation rule.
Now add a mutable list of routing labels inside every filter. The menu action still selects and duplicates the same way. The card’s copy policy and tests need to account for the new nested state. A spread or struct-element copy can silently keep a new reference field shared; field-by-field cloning is a policy you must maintain as the model grows.
What a clone cannot decide for the whole systemPersistence, resources, and changing templates
Copying database identifiers, version fields, timestamps, or audit history requires an explicit product decision. Fresh identity is this Duplicate action’s rule, not the definition of every clone.
A connection, subscription, file handle, or lock has its own lifecycle. Copying a containing record does not acquire an independent resource. Share through an explicit owner, reacquire through the resource’s API, exclude it, or reject duplication according to the type’s contract.
The current query is a small acyclic tree. An arbitrary graph may have cycles, intentional aliases, or references to IDs inside the graph. A recursive traversal needs to define which relationships it preserves and may need an identity map.
If another actor edits the source concurrently, obtaining one coherent snapshot requires synchronization or an immutable version.
Build UIs?Every structuredClone, postMessage, and pushState copies your state without asking the object how. A Duplicate button is where you decide.
Where it already is in your components
You probably already follow this rule: before you hand Svelte state to structuredClone, you pass it through $state.snapshot. The Svelte docs suggest it for exactly that call, and without it Chromium throws “#<Object> could not
be cloned.” You keep what you post to a worker or store with history.pushState plain data too. The reason is the question this lesson keeps
asking: who owns the copy rule?
structuredClone is a copier that lives outside your object. The HTML standard’s algorithm never calls a duplicate method. For an ordinary object it copies the own
enumerable properties it can read from outside into a new plain object. Methods live on
the prototype, so they stay behind and instanceof fails. Private # fields are not properties at all. Our IncidentCard keeps all of its state in
private fields, so its structured clone is {}. A class with $state fields ends up the same way, because Svelte compiles each field to private storage behind a
getter and setter. Even $state.snapshot of that instance returns {} without a
warning, unless the class defines toJSON.
A function is where the copier stops: the algorithm throws a DataCloneError for anything callable, so an onSelect handler tucked into the state is an
error rather than a missing field. postMessage and history.pushState run the same algorithm, and Chromium’s message changes only the method name: “Failed to execute
'pushState' on 'History': () => card.view() could not be cloned.”
import { IncidentCard } from '../card';
// Breaks: structuredClone copies own enumerable properties into a plain object.
// Every IncidentCard field is private, so the copy is {}: no query, no duplicate().
export function copyWithPlatform(card: IncidentCard) {
return structuredClone(card);
}
// Throws DataCloneError: the algorithm has no rule for copying a function.
export function copyWithHandler(card: IncidentCard) {
return structuredClone({ view: card.view(), onSelect: () => card.view() });
}
// Works: the instance that owns the state runs its own copy rule.
export function copyWithPrototype(card: IncidentCard, id: string) {
return card.duplicate(id);
}
When you have to own it
Now you’re building a survey editor, and each question is a class with $state fields: its prompt, its answer choices, whether an answer is required, and the response count
from the live survey. The Duplicate button beside a question is a Prototype decision, and structuredClone has already told you it cannot make it. Give the question a duplicate method and decide there what crosses.
The prompt, the required flag, and the choices copy, with a new choices array so editing the copy’s options leaves the original alone. The response count belongs to the original question, so the copy starts at zero. The copy needs its own ID, created once in the click handler and used as its list key. React’s docs warn that keys generated during rendering never match between renders, so every row is recreated and loses its input. If saving the new question fails and you retry, the retry is another attempt at the same duplicate, so it keeps the same idempotency key.
When all you need is an independent draft of plain data while a dialog is open, $state.snapshot or a small record copy is enough. Prototype earns its place when
the configured instance owns state the outside copier cannot see.
06 / Already in your toolbox
Read what an API means by “clone.”
Each of these APIs makes a different copying boundary visible.
Go · http.Request.Clone
Creates a request copy with a supplied context, while its documented exception keeps the Body shallowly copied. A new request object therefore does not mean an independently replayable body stream. This is a useful example of an explicit copy contract.
Read the request copy contract ↗Rust · Clone
An explicit clone calls each field’s copy operation. An owned value can
duplicate data, while a shared handle such as Rc keeps the same referent. The copy
contract must say which one is intended.
React · cloneElement
Creates a React element description with shallowly merged props. It is a nearby copying API; it does not duplicate a mounted component’s state. React documents alternatives and warns that this API can make data flow harder to follow.
Read the element API and alternatives ↗A useful counterexample: Object.createDelegation through the prototype chain
The ECMAScript specification defines Object.create(proto) as creating an object with the specified prototype.
A shallow spread copies an ordinary object’s own enumerable properties, while prototype delegation changes where property lookup can find values. Both can leave nested data shared for different reasons. Neither operation automatically implements this card’s selective duplication contract.
07 / When it earns its place
Choose it when the existing state is the useful starting point.
Prototype fits editor duplicates, runtime-configured templates, and known-good fixtures when callers should preserve the current instance’s configuration without reconstructing its internals. It is less useful when every new instance starts from a few fixed defaults or when sharing an immutable value already satisfies the requirement.
Copying is not automatically cheaper than construction. This card allocates a new editable query and copies its filters; the cost grows with those fields. The read-only definition is shared. A large graph may make immutability, structural sharing, or a documented copy-on-write strategy more appropriate. Measure the costs that matter.
Keep a literal or a small copy function when the state is simple and the copy rule has one clear caller. Use a factory when creation primarily resolves defaults or selects an implementation. Use a builder when pieces arrive in stages. A factory can select a prototype and ask it to duplicate; the patterns can cooperate without becoming interchangeable names.
08 / Take the idea with you
Explain who owns the copy rule.
Try explaining this without the pattern name: “The editor chooses an existing configured card. The card creates a new entity with independent query data, a shared read-only definition, and no inherited preview.” Then name the future field that would make that rule incomplete.
Connections to follow nextRelated lessons
Copying, identity & equality gives the underlying distinctions between equal values, separate objects, and shared state. Prototype applies those distinctions to a creation responsibility.
Builder assembles a result from steps; Prototype starts from a result that already exists. A copied configuration could seed a new builder, with the handoff’s ownership made explicit.
Flyweight deliberately shares common state across instances. The shared data definition here is a related choice; sharing it does not make the independently editable query a shared object.