Skip to content
Commitwise
Menu
Design Patterns

Prototype

Create new objects by copying an existing one, such as duplicating an invoice or starting a workout from a template, understand shallow and deep copies, give every copy its own identity, and avoid Java's clone(), in Swift, Kotlin, Java and TypeScript.

beginner

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

Users of an invoicing app send almost the same invoice every month: same client, same line items, new date. So the app gets a “Duplicate” button. The first version copies the invoice and opens the copy for editing. A week later, support gets the first ticket: “I changed the quantity on the new invoice and the old one changed too.” The next ticket is worse: “I saved the duplicate and my original invoice disappeared.”

Both bugs are about one question: what exactly does “copy” copy? The Prototype pattern creates new objects by copying an existing one, and getting it right means answering that question on purpose.

New to some of these? Open what you need, or skip ahead.

Basics:What are value types and reference types?

A value type is copied when you assign it: var b = a gives b its own copy, and changing b doesn’t change a. Swift structs and enums work this way.

A reference type is shared: val b = a makes b point to the same object, so changing b changes what a sees. Classes in Swift, Kotlin, Java and TypeScript objects and arrays work this way. The Value vs reference types tutorial covers it in depth.

Part Not started

Shallow and deep copies

Photocopying a form that has a sticky note on it gives you a picture of the sticky note, not a new sticky note. That’s fine for paper. In code, a shallow copy is worse than a photocopy: the new object gets its own top-level fields, but anything nested (a list, another object) is the same nested thing, shared by the original and the copy. Change the list through the copy, and the original’s list changes, because there is only one list.

A deep copy also copies the nested things, so the original and the copy have nothing in common and can change independently.

Prototype specifies the kinds of objects to create using a prototypical instance and creates new objects by copying it. The interesting decisions are about copy depth and identity: which fields are copied by value, which nested objects are cloned, which are deliberately shared (immutable data, references to other aggregates), and which must be reset (ids, timestamps, status). Language defaults are almost all shallow: Kotlin data class copy(), TypeScript object spread and Object.assign, Java Object.clone(). Swift structs are the exception that proves the rule: copying is deep exactly as far as the fields are value types, and stops at the first class reference.

// Swift: with a class for line items, a struct copy still shares them
final class LineItem {
    var description: String
    var quantity: Int
    init(description: String, quantity: Int) { self.description = description; self.quantity = quantity }
}

struct Invoice {
    var id: UUID
    var client: String
    var items: [LineItem]          // an array of references
}

var copy = original                // copies the array, not the LineItem objects
copy.items[0].quantity = 5         // original.items[0].quantity is 5 too

// Fix: make LineItem a struct. Now the copy is fully independent
struct LineItemValue { var description: String; var quantity: Int }
data class LineItem(var description: String, var quantity: Int)
data class Invoice(val id: UUID, val client: String, val items: MutableList<LineItem>)

val copy = original.copy()          // shallow: the same MutableList, the same LineItems
copy.items[0].quantity = 5           // original.items[0].quantity is 5 too
copy.items += LineItem("Extra", 1)   // the original gets the extra line too

// Fix: immutable data, so sharing is safe
data class LineItemV2(val description: String, val quantity: Int)
data class InvoiceV2(val id: UUID, val client: String, val items: List<LineItemV2>)
public final class Invoice {
    UUID id; String client; List<LineItem> items;

    // Shallow: both invoices hold the same list object
    Invoice shallowCopy() {
        var c = new Invoice(); c.id = id; c.client = client; c.items = items; return c;
    }
}

// Fix: records and immutable lists, so sharing is safe
public record LineItem(String description, int quantity) {}
public record InvoiceV2(UUID id, String client, List<LineItem> items) {
    public InvoiceV2 { items = List.copyOf(items); }   // defensive, unmodifiable copy
}
interface LineItem { description: string; quantity: number }
interface Invoice { id: string; client: string; items: LineItem[] }

const copy = { ...original };        // shallow: the same items array, the same objects
copy.items[0].quantity = 5;           // original.items[0].quantity is 5 too
copy.items.push({ description: "Extra", quantity: 1 });   // and the original gets it

// Fix: copy the nested parts you will change, or use structuredClone for plain data
const deep = { ...original, items: original.items.map((i) => ({ ...i })) };
const alsoDeep = structuredClone(original);
  • The copy looks independent: it has its own variable, and its top-level fields can be changed safely.
  • But items is a reference: both invoices point to the same list of the same line items.
  • The simplest fix is immutable data: if line items can’t be changed in place, sharing them is harmless, and “changing” one means replacing it with a new one in the copy’s own list.
  • When the data must be mutable, copy the nested parts explicitly.
Spot the bugSaving the duplicate deleted the original

Tap the line with the bug.

struct Invoice {
    var id: UUID
    var number: String
    var client: String
    var items: [LineItem]
    var status: Status
}

func duplicate(_ original: Invoice) -> Invoice {
    var copy = original
    copy.status = .draft
    return copy
}
data class Invoice(
    val id: UUID,
    val number: String,
    val client: String,
    val items: List<LineItem>,
    val status: Status,
)

fun duplicate(original: Invoice): Invoice =
    original.copy(status = Status.DRAFT)
public record Invoice(
    UUID id,
    String number,
    String client,
    List<LineItem> items,
    Status status) {

    public Invoice duplicate() {
        return new Invoice(id, number, client, items, Status.DRAFT);
    }
}
interface Invoice {
  id: string;
  number: string;
  client: string;
  items: readonly LineItem[];
  status: Status;
}

function duplicate(original: Invoice): Invoice {
  return { ...original, status: "draft" };
}

Predict the output

Swift: Invoice is a struct, and LineItem is now also a struct. You write var copy = original, then copy.items[0].quantity = 5. What is original.items[0].quantity?

  • Prototype creates objects by copying an existing instance; the real decisions are copy depth and identity.
  • Defaults are shallow: Kotlin copy(), TS spread/Object.assign, Java clone(). Swift structs copy deeply up to the first class reference (copy-on-write for collections).
  • Prefer immutable nested data so sharing is safe; deep-copy only what will be mutated (structuredClone for plain data in TS).
  • Reset identity and history on copy: id, number, timestamps, status, external references.

Check this part

  1. After a shallow copy, what do the original and the copy share?

  2. Why is sharing nested data safe when it is immutable?

  3. Which of these is a deep copy by default?

A prototype is an object you copy to make a new one. A shallow copy copies the fields, so nested mutable objects are still shared; a deep copy copies them too. Kotlin's copy(), TypeScript's spread and Java's clone() are shallow; Swift structs with value-type fields copy deeply for free.

Part Not started

Templates, registries and how to write the copy

The other common use of Prototype is templates. A fitness app has ready-made workouts (“Beginner full body”, “5 km run”) with all their exercises, sets and rests. When a user starts one, the app copies the template and the user changes the copy: fewer sets, a different weight. The template stays untouched for the next person.

Keeping a few ready objects in a dictionary by name and copying the one you need is called a prototype registry. It’s handy when objects are complicated to set up and differ only a little.

A prototype registry maps keys to preconfigured instances and returns copies. It replaces a hierarchy of subclasses or a long constructor when objects differ by configuration rather than behavior: workout and document templates, default dashboards, game entities, test fixtures. The registry must never hand out the prototype itself, or the first caller to mutate it changes every later copy. As for writing the copy: in Java, don’t use Cloneable/Object.clone(): it bypasses constructors, is shallow, can’t be used with final fields that need new values, and forces CloneNotSupportedException handling. Use a copy constructor or a static copyOf, which make depth and identity explicit and play well with records and immutability.

struct Workout {
    var id = UUID()
    var name: String
    var exercises: [Exercise]       // structs: copying is deep
}

final class WorkoutTemplates {
    private let templates: [String: Workout]
    init(templates: [String: Workout]) { self.templates = templates }

    // Returns a copy with a new identity; the template itself never leaves the registry
    func start(_ key: String) -> Workout? {
        guard var workout = templates[key] else { return nil }
        workout.id = UUID()
        return workout
    }
}
data class Workout(val id: UUID, val name: String, val exercises: List<Exercise>)

class WorkoutTemplates(private val templates: Map<String, Workout>) {
    // Returns a copy with a new identity; immutable exercises are safely shared
    fun start(key: String): Workout? =
        templates[key]?.copy(id = UUID.randomUUID())
}
public record Workout(UUID id, String name, List<Exercise> exercises) {
    public Workout { exercises = List.copyOf(exercises); }

    // A copy method instead of clone(): explicit about what is new
    public Workout startedCopy() {
        return new Workout(UUID.randomUUID(), name, exercises);
    }
}

public final class WorkoutTemplates {
    private final Map<String, Workout> templates;
    public WorkoutTemplates(Map<String, Workout> templates) { this.templates = Map.copyOf(templates); }

    public Optional<Workout> start(String key) {
        return Optional.ofNullable(templates.get(key)).map(Workout::startedCopy);
    }
}
interface Workout { id: string; name: string; exercises: Exercise[] }

class WorkoutTemplates {
  constructor(private readonly templates: ReadonlyMap<string, Workout>) {}

  // Deep-copies because exercises are mutable plain objects the user will edit
  start(key: string): Workout | undefined {
    const template = this.templates.get(key);
    if (!template) return undefined;
    return { ...structuredClone(template), id: crypto.randomUUID() };
  }
}

Production puzzles

Predict the output

TypeScript: a template contains startsAt: new Date(...) and an onComplete callback function. What does structuredClone(template) do?

Predict the output

Java: class Invoice implements Cloneable with super.clone(). Its items field is final List<LineItem>. How do you give the clone its own list?

Kotlin note

data class copy() only exists for properties declared in the primary constructor, and it’s generated as public. If a class has invariants (an id that must change, a list that must be copied), a public copy() lets any caller skip them. Since Kotlin 2.0.20 the compiler warns when a data class with a private constructor exposes copy(); with @ConsistentCopyVisibility the generated copy() gets the constructor’s visibility, so callers have to use your own copy method.

Swift note

A struct with a class field gives you a shallow copy without any warning. If you need a reference type inside a value type (for performance, for example), implement copy-on-write yourself: before mutating, check isKnownUniquelyReferenced(&storage) and copy the storage if it’s shared. That’s how Array and String behave like values while sharing memory.

Exerciseeasy

Duplicate a project

A task app lets users duplicate a project. A project has an id, a name, a list of tasks (each with its own id, a title, a done flag and an assignee) and a creation date. For each field, decide: copy as is, copy deeply, or reset?

›

Ask two questions per field: will the user change it in the copy, and should the copy and the original ever have the same value?

SolutionTry it yourself first
  • Project id: reset to a new id; otherwise saving the copy overwrites the original.
  • Name: copy, maybe with ” (copy)” appended so the user can tell them apart.
  • Tasks: copy deeply, because the user will tick them off in the new project. Each task also needs a new id and done = false.
  • Assignee: copy the reference (the same user), since people are shared, not duplicated.
  • Creation date: reset to now.
  • Prototype registry: keyed preconfigured instances; always return copies, never the prototype.
  • Fits objects that differ by configuration, not behavior (templates, defaults, fixtures).
  • Java: avoid Cloneable; use copy constructors or copyOf (constructors run, final fields work, depth explicit).
  • TS structuredClone: no functions (throws), no class prototypes. Kotlin copy() is public and bypasses invariants; @ConsistentCopyVisibility.
  • Swift: class fields inside structs are shared; implement copy-on-write with isKnownUniquelyReferenced.

Check this part

  1. What must a prototype registry never return?

  2. Why prefer a copy constructor over Java's clone()?

  3. What does structuredClone do with a function property?

Prototypes are useful as preconfigured templates: keep a few ready objects, copy one and customize it. Expose copying through your own method or copy constructor, not Java's Cloneable. Copy constructors and copy functions make depth and identity explicit.

Key takeaways

  • Prototype makes new objects by copying an existing one.
  • A shallow copy shares nested objects; a deep copy doesn’t.
  • Immutable nested data can be shared safely.
  • Every copy needs its own id, and fields like dates and status reset.
  • The real design decisions are copy depth and identity.
  • Language defaults are shallow, except Swift value types.
  • Registries hand out copies of preconfigured templates.
  • Write explicit copy functions; avoid Java Cloneable.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.