← Design patterns
Creation Existing instances and deliberate copies

Prototype

Start from something that already exists.

You have an incident card configured just the way you want it: a time window, a set of filters, and a data source. Now you want another card like it. Should the editor reconstruct those choices, or should the existing card know how to make a useful copy?

TypeScriptGoPythonOne copy contract, across the comparison.

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.

The incident card's duplication contract
Part of the cardWhat the duplicate receivesWhy
Title and query settingsCurrent values, with independent editable query storageLater edits belong to one card.
Data definitionThe same read-only definitionSharing is intentional; the card cannot edit it.
Card IDA different ID supplied by the callerThis product action creates another entity.
Last previewNo preview yetA previous result belongs to the source’s runtime state.

An existing card creates another card with its title and a caller-supplied new ID.

TypeScriptReading
card.ts
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);
	}
}
GoAlongside
card.go
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.

Prototype

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 min
  • http-5xxenabled
  • timeoutenabled

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.

01/ 04
Start configured

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
card.ts · shallow comparison
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.

What should “Duplicate card” promise?

The user wants another independently editable incident card. The data definition is read-only and the original has a cached preview. Which copy contract fits that action?

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.”

clone.ts
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.

Read the Clone contract ↗

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.

Start from an existing instance. Say which values become independent, which stay shared, and which must change.

Back to design patterns →