InterviewKata
Design patterns · 04 of 26 · Making objects

Prototype

Prototype makes new objects by copying a configured one and changing only what differs. This page builds Pedal's fare plans from a registered standard plan, and shows the bug every prototype risks: a shallow copy that edits the original through a shared list.

1. The pain

Pedal's standard fare plan has fourteen settings: unlock fee, price per minute, free minutes, two peak windows with their own prices, daily and monthly caps, and more. Every month the operations team adds a plan (student, weekend, corporate) that differs from the standard one in two or three of them.

Typing all fourteen again each time is slow, and every retyped setting is a chance for a mistake. What the team really means is "the standard plan, but…". The code should say that too.

2. The idea in one picture

When you need a new contract, you don't write it from a blank page. You copy last year's signed one, change the names and dates, and keep everything else. The old contract is untouched; the copy is yours to edit.

Prototype creates new objects by copying an existing, fully configured one, then changing what differs. The object knows how to copy itself (a clone() method), so the caller doesn't need to know its class or its fields. A registry often keeps the ready-made prototypes by name.

The catch is the depth of the copy. A shallow copy makes a new outer object but shares the lists and maps inside it, so changing the copy's caps changes the original's too. A deep copy copies those as well. Section 6 lets you see the difference.

3. The roles

RoleIts jobIn Pedal
PrototypeDeclares the copy operation.FarePlan.clone(name)
Concrete prototypeA configured object that copies itself, deeply enough to be independent.The standard plan
Registry (optional)Holds prototypes by name and hands out copies, never the original.PlanRegistry
ClientAsks for a copy and adjusts it.The ops tool that creates the student plan

4. Before and after

Creating the student plan both ways (Python sketch; the full program is in section 5).

Without the pattern

student = FarePlan(
    "student", unlock=0, per_minute=10,
    free_minutes=0, peak_am=120,
    peak_pm=None,          # forgotten
    daily_cap=600, monthly_cap=4000,
    # ... seven more settings,
    # all copied by eye
)

With it

student = registry.clone("standard",
                         "student")
student.unlock = 0
student.peak["07-09"] = 120
student.caps[0] = ("daily", 600)
The next changeWithout the patternWith it
A weekend planAnother fourteen settings typed by handClone the standard plan, change two
The standard plan gains a settingEvery plan's creation code must add itClones pick it up automatically
Plans created by users in an admin screenThe screen must know every field"Duplicate" calls clone()

Copying keeps the shared settings in one place: the prototype. The copy states only what's different.

5. The code

A registry holds the standard plan; the student plan is a deep copy with three changes, and the standard plan is unchanged. Then a shallow copy adds a cap, and the standard plan gets it too.

from __future__ import annotations
import copy


class FarePlan:
    """A configured object that is easier to copy and tweak than to build from scratch."""

    def __init__(self, name: str, unlock: int, per_minute: int, peak: dict[str, int], caps: list[tuple[str, int]]) -> None:
        self.name = name
        self.unlock = unlock
        self.per_minute = per_minute
        self.peak = peak            # hours -> price as a percentage of normal
        self.caps = caps            # (period, most a rider pays in that period)

    def clone(self, name: str) -> FarePlan:
        twin = copy.deepcopy(self)  # new dict and list: the copy shares nothing that can change
        twin.name = name
        return twin

    def shallow_copy(self, name: str) -> FarePlan:
        twin = copy.copy(self)      # new object, but the same dict and list inside it
        twin.name = name
        return twin

    def describe(self) -> str:
        peak = ", ".join(f"{h} {p}%" for h, p in self.peak.items())
        caps = ", ".join(f"{n} {p}p" for n, p in self.caps)
        return f"{self.name}: unlock {self.unlock}p, {self.per_minute}p/min, peak {peak}, caps {caps}"


class PlanRegistry:
    """Keeps ready-made prototypes by name; new plans start as copies of them."""

    def __init__(self) -> None:
        self.prototypes: dict[str, FarePlan] = {}

    def add(self, plan: FarePlan) -> None:
        self.prototypes[plan.name] = plan

    def clone(self, key: str, new_name: str) -> FarePlan:
        if key not in self.prototypes:
            raise KeyError(f"no prototype called '{key}'")
        return self.prototypes[key].clone(new_name)


if __name__ == "__main__":
    registry = PlanRegistry()
    standard = FarePlan("standard", 100, 10, {"07-09": 150, "17-19": 150}, [("daily", 1200)])
    registry.add(standard)
    print(standard.describe())

    student = registry.clone("standard", "student")
    student.unlock = 0
    student.peak["07-09"] = 120
    student.caps[0] = ("daily", 600)
    print(student.describe())
    print(f"after the deep copy -> {standard.describe()}")

    weekend = standard.shallow_copy("weekend")
    weekend.caps.append(("weekend", 800))
    print(f"after the shallow copy -> {standard.describe()}")
    try:
        registry.clone("night", "late")
    except KeyError as e:
        print(f"rejected: {e.args[0]}")
type Cap = readonly [string, number];

// A configured object that is easier to copy and tweak than to build from scratch.
class FarePlan {
  constructor(
    public name: string,
    public unlock: number,
    public perMinute: number,
    public peak: Map<string, number>,   // hours -> price as a percentage of normal
    public caps: Cap[],                 // (period, most a rider pays in that period)
  ) {}

  clone(name: string): FarePlan {
    // new map and array: the copy shares nothing that can change
    return new FarePlan(name, this.unlock, this.perMinute, new Map(this.peak), [...this.caps]);
  }

  shallowCopy(name: string): FarePlan {
    // new object, but the same map and array inside it
    return new FarePlan(name, this.unlock, this.perMinute, this.peak, this.caps);
  }

  describe(): string {
    const peak = [...this.peak].map(([h, p]) => `${h} ${p}%`).join(", ");
    const caps = this.caps.map(([n, p]) => `${n} ${p}p`).join(", ");
    return `${this.name}: unlock ${this.unlock}p, ${this.perMinute}p/min, peak ${peak}, caps ${caps}`;
  }
}

// Keeps ready-made prototypes by name; new plans start as copies of them.
class PlanRegistry {
  private readonly prototypes = new Map<string, FarePlan>();

  add(plan: FarePlan): void { this.prototypes.set(plan.name, plan); }

  clone(key: string, newName: string): FarePlan {
    const proto = this.prototypes.get(key);
    if (!proto) throw new Error(`no prototype called '${key}'`);
    return proto.clone(newName);
  }
}

const registry = new PlanRegistry();
const standard = new FarePlan("standard", 100, 10, new Map([["07-09", 150], ["17-19", 150]]), [["daily", 1200]]);
registry.add(standard);
console.log(standard.describe());

const student = registry.clone("standard", "student");
student.unlock = 0;
student.peak.set("07-09", 120);
student.caps[0] = ["daily", 600];
console.log(student.describe());
console.log(`after the deep copy -> ${standard.describe()}`);

const weekend = standard.shallowCopy("weekend");
weekend.caps.push(["weekend", 800]);
console.log(`after the shallow copy -> ${standard.describe()}`);
try {
  registry.clone("night", "late");
} catch (e) {
  console.log(`rejected: ${(e as Error).message}`);
}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Main {
    record Cap(String period, int pence) {}

    // A configured object that is easier to copy and tweak than to build from scratch.
    static class FarePlan {
        String name;
        int unlock, perMinute;
        final Map<String, Integer> peak;   // hours -> price as a percentage of normal
        final List<Cap> caps;              // (period, most a rider pays in that period)

        FarePlan(String name, int unlock, int perMinute, Map<String, Integer> peak, List<Cap> caps) {
            this.name = name; this.unlock = unlock; this.perMinute = perMinute; this.peak = peak; this.caps = caps;
        }

        // A copy method rather than Object.clone(): explicit about what is copied.
        FarePlan copy(String newName) {
            // new map and list: the copy shares nothing that can change
            return new FarePlan(newName, unlock, perMinute, new LinkedHashMap<>(peak), new ArrayList<>(caps));
        }

        FarePlan shallowCopy(String newName) {
            // new object, but the same map and list inside it
            return new FarePlan(newName, unlock, perMinute, peak, caps);
        }

        String describe() {
            String p = peak.entrySet().stream().map(e -> e.getKey() + " " + e.getValue() + "%").collect(Collectors.joining(", "));
            String c = caps.stream().map(x -> x.period() + " " + x.pence() + "p").collect(Collectors.joining(", "));
            return name + ": unlock " + unlock + "p, " + perMinute + "p/min, peak " + p + ", caps " + c;
        }
    }

    // Keeps ready-made prototypes by name; new plans start as copies of them.
    static class PlanRegistry {
        private final Map<String, FarePlan> prototypes = new HashMap<>();

        void add(FarePlan plan) { prototypes.put(plan.name, plan); }

        FarePlan copy(String key, String newName) {
            FarePlan proto = prototypes.get(key);
            if (proto == null) throw new IllegalArgumentException("no prototype called '" + key + "'");
            return proto.copy(newName);
        }
    }

    public static void main(String[] args) {
        PlanRegistry registry = new PlanRegistry();
        Map<String, Integer> peak = new LinkedHashMap<>();
        peak.put("07-09", 150);
        peak.put("17-19", 150);
        FarePlan standard = new FarePlan("standard", 100, 10, peak, new ArrayList<>(List.of(new Cap("daily", 1200))));
        registry.add(standard);
        System.out.println(standard.describe());

        FarePlan student = registry.copy("standard", "student");
        student.unlock = 0;
        student.peak.put("07-09", 120);
        student.caps.set(0, new Cap("daily", 600));
        System.out.println(student.describe());
        System.out.println("after the deep copy -> " + standard.describe());

        FarePlan weekend = standard.shallowCopy("weekend");
        weekend.caps.add(new Cap("weekend", 800));
        System.out.println("after the shallow copy -> " + standard.describe());
        try {
            registry.copy("night", "late");
        } catch (IllegalArgumentException e) {
            System.out.println("rejected: " + e.getMessage());
        }
    }
}

Output (the same in all three languages)

standard: unlock 100p, 10p/min, peak 07-09 150%, 17-19 150%, caps daily 1200p
student: unlock 0p, 10p/min, peak 07-09 120%, 17-19 150%, caps daily 600p
after the deep copy -> standard: unlock 100p, 10p/min, peak 07-09 150%, 17-19 150%, caps daily 1200p
after the shallow copy -> standard: unlock 100p, 10p/min, peak 07-09 150%, 17-19 150%, caps daily 1200p, weekend 800p
rejected: no prototype called 'night'

6. Try it

Shallow or deep?

Make a copy of the standard plan, then change the copy. Watch what happens to the original.

Dotted underline: a map or list the two plans share. Red: a value in the original that changed.

7. When not to use it

What you get

  • Less to type, less to get wrong: copies state only what differs.
  • No class knowledge needed: the object copies itself.
  • Cheap creation: copying beats rebuilding when setup is slow (loading, parsing, computing).
  • Runtime variety: users can duplicate and edit plans without new classes.

What you pay

  • Copy depth is a trap: shared inner lists and maps leak changes back.
  • Identity must be reset: ids, database keys and timestamps shouldn't be copied blindly.
  • Cycles and resources: objects holding connections, files or back-references are hard to copy.
  • Java's clone() is awkward: prefer a copy constructor or copy method.
If…Use instead
New objects need many optional parts set from scratch03 Builder
The variations are fixed and known in advanceNamed constructors or factory functions (FarePlan.student())
The object is immutableCopy-with-changes (dataclasses.replace, records with with-style methods)
Only one object should exist05 Singleton

8. How it goes wrong

Shallow copies of mutable insidesThe copy and the original share lists and maps, so editing one edits both. Copy deeply enough that nothing mutable is shared.
Handing out the prototype itselfIf the registry returns the original instead of a copy, the first caller to tweak it changes every later plan.
Copying identityA cloned plan with the original's database id overwrites the original on save. Reset ids and audit fields in clone().
Using Java's CloneableObject.clone() is shallow by default and bypasses constructors. A copy method or copy constructor is clearer.

9. Remember it and say it

Memory hook: "Copy the signed contract"Start from a finished one, change what differs, and make sure the copy shares nothing.
How this hook works: what “Copy the signed contract” means

What the slogan means, phrase by phrase

“Copy”
clone(): the object copies itself.
“the signed contract”
A fully configured prototype.
“shares nothing”
A deep enough copy.

Why this picture fits: Nobody drafts a contract from a blank page when a good one already exists.

The story → the concept

The officeThe code
Last year's contractThe standard plan
The filing cabinetPlanRegistry
A photocopyA deep copy
Two people editing the same sheetA shallow copy's shared list

Watch it on an example

  1. Ops clones the standard plan as "student".
  2. They change the unlock fee, the morning peak and the daily cap.
  3. The standard plan is unchanged, because the copy's maps and lists are its own.

The trap it saves you from

A shallow copy that quietly edits the original through a shared list.

It still works when…

Yes: "Duplicate" buttons, game entity templates and test fixtures all work this way.

In an interview or code review

10. Check yourself

What does prototype create new objects from?

Then only the differences are changed.

After a shallow copy, the copy appends a cap. What happens to the original?

A shallow copy shares the inner list.

Why should a registry hand out copies rather than the prototype?

The prototype must stay pristine.

What should clone() usually reset?

Otherwise saving the copy overwrites the original.

Your fare plan objects are immutable. What replaces clone()?

Immutable objects have no shared-state risk.

11. Go deeper

ResourceWhy
03 BuilderThe other way to make complex objects: assemble instead of copy.
Python copy moduleShallow and deep copies, and how classes customise them.
MDN: structuredClone()Deep copying in JavaScript and TypeScript.
Wikipedia: Prototype patternStructure and language notes.