Prototype Pattern Explained

Introduction
Some objects are expensive to create — they require database queries, complex configuration, or a series of calculations to get into a valid state. Once you have one good instance, why throw it away and build another from scratch?
The Prototype Pattern answers this question: instead of creating a new object, clone an existing one and modify only what's different. It's deceptively simple and surprisingly powerful.
What You'll Learn
✅ Understand the problem the Prototype pattern solves
✅ Implement shallow copy vs deep copy correctly
✅ Build a Prototype Registry for managed cloning
✅ Apply the pattern in TypeScript, Python, and Java
✅ Recognize Prototype in the wild (JavaScript's prototypal inheritance, game engines)
✅ Know when to use Prototype vs other creational patterns
Prerequisites
- Completed Builder Pattern Explained
- Familiar with Factory and Abstract Factory Patterns
- Comfortable with classes and objects (Classes, Objects, and Abstraction)
The Problem: Expensive Object Creation
Consider a game with enemy units. Each Enemy has stats, AI behavior trees, textures, pathfinding graphs, and sound profiles — all loaded from disk and computed at initialization:
class Enemy {
stats: Stats;
behaviorTree: BehaviorTree;
textures: TextureSet;
pathfindingGraph: Graph;
constructor(config: EnemyConfig) {
this.stats = loadStatsFromDisk(config.statFile); // slow
this.behaviorTree = parseBehaviorXML(config.aiFile); // slow
this.textures = loadTextures(config.textureDir); // very slow
this.pathfindingGraph = computeGraph(config.mapData); // CPU-intensive
}
}
// Spawning 50 enemies per wave = 50 × (all that expensive initialization)
for (let i = 0; i < 50; i++) {
enemies.push(new Enemy(goblinConfig)); // ❌ painfully slow
}The solution: load one Enemy once, then clone it 49 more times:
const template = new Enemy(goblinConfig); // ✅ load once
for (let i = 0; i < 50; i++) {
const enemy = template.clone(); // fast copy
enemy.position = randomSpawnPoint();
enemies.push(enemy);
}This is the Prototype pattern in one sentence: create new objects by copying a prototype.
Pattern Structure
The Prototype pattern has a minimal structure:
Key participants:
- Prototype — interface declaring the
clone()method - ConcretePrototype — implements
clone(), knows how to copy itself - Client — calls
clone()instead of callingnew
The pattern delegates the copying responsibility to the object itself — it knows its own internal state best.
Shallow Copy vs Deep Copy
This is the most critical concept in the Prototype pattern. Get it wrong and you'll have objects sharing state in ways that cause subtle, hard-to-debug bugs.
Shallow Copy
A shallow copy duplicates the object's top-level fields. References (pointers) to nested objects are copied — not the nested objects themselves. Both the original and the clone point to the same nested objects.
class Address {
constructor(
public street: string,
public city: string
) {}
}
class User {
constructor(
public name: string,
public address: Address
) {}
shallowClone(): User {
return Object.assign(new User("", new Address("", "")), this);
// Or simply: return { ...this } as User
}
}
const original = new User("Alice", new Address("123 Main St", "Springfield"));
const clone = original.shallowClone();
clone.name = "Bob"; // ✅ independent — primitive value
clone.address.city = "Shelbyville"; // ❌ modifies original.address.city too!
console.log(original.address.city); // "Shelbyville" — oops!Visualizing shallow copy:
Deep Copy
A deep copy recursively duplicates all nested objects. The clone is completely independent.
class Address {
constructor(
public street: string,
public city: string
) {}
clone(): Address {
return new Address(this.street, this.city);
}
}
class User {
constructor(
public name: string,
public address: Address
) {}
deepClone(): User {
return new User(this.name, this.address.clone()); // clone nested objects too
}
}
const original = new User("Alice", new Address("123 Main St", "Springfield"));
const clone = original.deepClone();
clone.name = "Bob";
clone.address.city = "Shelbyville";
console.log(original.address.city); // "Springfield" ✅ original untouchedWhen to Use Each
| Scenario | Use |
|---|---|
| All fields are primitives (strings, numbers, booleans) | Shallow copy is safe |
| Fields are references to immutable objects | Shallow copy is safe |
| Fields are references to mutable objects | Deep copy required |
| Nested objects contain nested objects | Deep copy required |
| Performance is critical and mutation won't happen | Shallow copy for speed |
TypeScript Implementation
Basic Prototype with Interface
interface Cloneable<T> {
clone(): T;
}
class GameCharacter implements Cloneable<GameCharacter> {
constructor(
public name: string,
public health: number,
public attack: number,
public skills: string[], // mutable array — needs deep copy
public position: { x: number; y: number } // mutable object — needs deep copy
) {}
clone(): GameCharacter {
return new GameCharacter(
this.name,
this.health,
this.attack,
[...this.skills], // deep copy array
{ ...this.position } // deep copy object
);
}
}
// Usage
const goblinTemplate = new GameCharacter(
"Goblin",
100,
15,
["slash", "dodge"],
{ x: 0, y: 0 }
);
const goblin1 = goblinTemplate.clone();
goblin1.position = { x: 10, y: 20 };
goblin1.health = 80; // wounded
const goblin2 = goblinTemplate.clone();
goblin2.position = { x: 50, y: 30 };
// Template unchanged
console.log(goblinTemplate.health); // 100
console.log(goblinTemplate.position); // { x: 0, y: 0 }Prototype Registry
A Prototype Registry (also called a Prototype Store or Cache) maintains a collection of pre-built prototypes keyed by name. Clients request clones by key:
class CharacterRegistry {
private prototypes = new Map<string, GameCharacter>();
register(key: string, prototype: GameCharacter): void {
this.prototypes.set(key, prototype);
}
create(key: string): GameCharacter {
const prototype = this.prototypes.get(key);
if (!prototype) {
throw new Error(`Prototype '${key}' not found in registry`);
}
return prototype.clone();
}
}
// Setup registry once (e.g., at game load)
const registry = new CharacterRegistry();
registry.register("goblin", new GameCharacter("Goblin", 100, 15, ["slash"], { x: 0, y: 0 }));
registry.register("troll", new GameCharacter("Troll", 500, 40, ["smash", "roar"], { x: 0, y: 0 }));
registry.register("archer", new GameCharacter("Archer", 80, 25, ["shoot", "dodge"], { x: 0, y: 0 }));
// Spawn enemies efficiently
function spawnWave(type: string, count: number, spawnPoints: { x: number; y: number }[]) {
return spawnPoints.slice(0, count).map((pos, i) => {
const enemy = registry.create(type);
enemy.position = pos;
enemy.name = `${enemy.name} #${i + 1}`;
return enemy;
});
}
const wave = spawnWave("goblin", 10, generateSpawnPoints());Document Template Example
A common real-world use case: email/document templates.
interface Section {
title: string;
content: string;
clone(): Section;
}
class TextSection implements Section {
constructor(
public title: string,
public content: string
) {}
clone(): TextSection {
return new TextSection(this.title, this.content);
}
}
class Document {
constructor(
public title: string,
public author: string,
public sections: Section[],
public metadata: Record<string, string>
) {}
clone(): Document {
return new Document(
this.title,
this.author,
this.sections.map(s => s.clone()), // deep clone sections
{ ...this.metadata } // shallow clone (values are strings)
);
}
}
// Create a template document
const reportTemplate = new Document(
"Monthly Report",
"Template",
[
new TextSection("Executive Summary", "Insert summary here..."),
new TextSection("Key Metrics", "Insert metrics here..."),
new TextSection("Conclusion", "Insert conclusion here..."),
],
{ company: "Acme Corp", confidential: "true" }
);
// Generate this month's report from the template
const janReport = reportTemplate.clone();
janReport.title = "Monthly Report - January 2026";
janReport.author = "Alice Johnson";
janReport.sections[0].content = "January was a record-breaking month...";
janReport.metadata.period = "January 2026";
// Template remains pristine for next month
console.log(reportTemplate.title); // "Monthly Report" — unchangedPython Implementation
Python provides built-in support for shallow and deep copying via the copy module.
import copy
from dataclasses import dataclass, field
from typing import Protocol
class Cloneable(Protocol):
def clone(self) -> "Cloneable":
...
@dataclass
class Skill:
name: str
damage: int
cooldown: float
def clone(self) -> "Skill":
return Skill(self.name, self.damage, self.cooldown)
@dataclass
class GameCharacter:
name: str
health: int
attack: int
skills: list[Skill] = field(default_factory=list)
position: dict = field(default_factory=lambda: {"x": 0, "y": 0})
def shallow_clone(self) -> "GameCharacter":
"""Shallow copy — skills list is shared!"""
return copy.copy(self)
def clone(self) -> "GameCharacter":
"""Deep copy — fully independent"""
return copy.deepcopy(self)
def manual_clone(self) -> "GameCharacter":
"""Manual deep clone — more control over what's copied"""
return GameCharacter(
name=self.name,
health=self.health,
attack=self.attack,
skills=[skill.clone() for skill in self.skills],
position=dict(self.position)
)
# Shallow copy pitfall
goblin_template = GameCharacter(
name="Goblin",
health=100,
attack=15,
skills=[Skill("slash", 20, 1.5), Skill("dodge", 0, 3.0)]
)
shallow = goblin_template.shallow_clone()
shallow.skills.append(Skill("backstab", 35, 5.0))
print(len(goblin_template.skills)) # 3 — template was modified! Bug!
# Deep copy — safe
goblin_template2 = GameCharacter(
name="Goblin",
health=100,
attack=15,
skills=[Skill("slash", 20, 1.5), Skill("dodge", 0, 3.0)]
)
deep = goblin_template2.clone()
deep.skills.append(Skill("backstab", 35, 5.0))
print(len(goblin_template2.skills)) # 2 — template untouched ✅Python Prototype Registry
from typing import Dict, TypeVar
T = TypeVar("T", bound=GameCharacter)
class CharacterRegistry:
def __init__(self):
self._prototypes: Dict[str, GameCharacter] = {}
def register(self, key: str, prototype: GameCharacter) -> None:
self._prototypes[key] = prototype
def create(self, key: str, **overrides) -> GameCharacter:
if key not in self._prototypes:
raise KeyError(f"Prototype '{key}' not registered")
char = self._prototypes[key].clone()
for attr, value in overrides.items():
setattr(char, attr, value)
return char
# Usage
registry = CharacterRegistry()
registry.register("goblin", GameCharacter("Goblin", 100, 15, [Skill("slash", 20, 1.5)]))
registry.register("elite_goblin", GameCharacter("Elite Goblin", 200, 30, [Skill("slash", 30, 1.0), Skill("shield", 0, 4.0)]))
# Spawn with customization
g1 = registry.create("goblin", name="Goblin Scout", position={"x": 10, "y": 5})
g2 = registry.create("goblin", health=50) # weakened variant
print(g1.name) # Goblin Scout
print(g2.health) # 50Using __copy__ and __deepcopy__ Hooks
Python lets you customize copy behavior with special methods:
import copy
class Configuration:
def __init__(self, settings: dict, cache: dict = None):
self.settings = settings
self.cache = cache or {} # cache should NOT be copied
def __copy__(self):
"""Shallow copy: share the cache"""
new_obj = Configuration.__new__(Configuration)
new_obj.__dict__.update(self.__dict__)
return new_obj
def __deepcopy__(self, memo):
"""Deep copy: fresh cache, deep copy settings, shared heavy resources"""
new_obj = Configuration.__new__(Configuration)
memo[id(self)] = new_obj
new_obj.settings = copy.deepcopy(self.settings, memo)
new_obj.cache = {} # don't copy the cache — intentionally fresh
return new_obj
config = Configuration({"theme": "dark", "language": "en"})
config.cache["user_prefs"] = {"font_size": 14}
cloned = copy.deepcopy(config)
cloned.settings["theme"] = "light"
print(config.settings["theme"]) # "dark" — original untouched ✅
print(len(cloned.cache)) # 0 — cache not copied as intended ✅Java Implementation
Java has a built-in Cloneable interface and Object.clone() method, though they come with caveats.
Using Cloneable (Classic Approach)
import java.util.ArrayList;
import java.util.List;
public class GameCharacter implements Cloneable {
private String name;
private int health;
private int attack;
private List<String> skills; // mutable — needs deep copy
public GameCharacter(String name, int health, int attack, List<String> skills) {
this.name = name;
this.health = health;
this.attack = attack;
this.skills = skills;
}
@Override
public GameCharacter clone() {
try {
GameCharacter clone = (GameCharacter) super.clone(); // shallow copy of primitives
clone.skills = new ArrayList<>(this.skills); // deep copy of list
return clone;
} catch (CloneNotSupportedException e) {
throw new AssertionError("Should never happen", e);
}
}
// Getters/setters omitted for brevity
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getHealth() { return health; }
public void setHealth(int health) { this.health = health; }
public List<String> getSkills() { return skills; }
}
// Usage
GameCharacter goblinTemplate = new GameCharacter(
"Goblin", 100, 15,
new ArrayList<>(List.of("slash", "dodge"))
);
GameCharacter goblin1 = goblinTemplate.clone();
goblin1.setName("Goblin Scout");
goblin1.getSkills().add("backstab");
// Template unaffected
System.out.println(goblinTemplate.getName()); // "Goblin"
System.out.println(goblinTemplate.getSkills().size()); // 2Note: Java's
Cloneableis widely considered a poorly designed interface. It has noclone()method itself — it merely signals toObject.clone()that cloning is allowed. The copy constructor pattern below is often preferred in modern Java.
Copy Constructor Pattern (Preferred in Modern Java)
public class GameCharacter {
private final String name;
private final int health;
private final int attack;
private final List<String> skills;
// Regular constructor
public GameCharacter(String name, int health, int attack, List<String> skills) {
this.name = name;
this.health = health;
this.attack = attack;
this.skills = List.copyOf(skills); // immutable copy
}
// Copy constructor — explicit and clear
public GameCharacter(GameCharacter other) {
this(other.name, other.health, other.attack, other.skills);
}
// Builder-style modification — returns new instance
public GameCharacter withName(String name) {
return new GameCharacter(name, this.health, this.attack, this.skills);
}
public GameCharacter withHealth(int health) {
return new GameCharacter(this.name, health, this.attack, this.skills);
}
public GameCharacter addSkill(String skill) {
List<String> newSkills = new ArrayList<>(this.skills);
newSkills.add(skill);
return new GameCharacter(this.name, this.health, this.attack, newSkills);
}
public String getName() { return name; }
public int getHealth() { return health; }
public List<String> getSkills() { return skills; }
}
// Usage — immutable + copy constructor = clean cloning
GameCharacter goblinTemplate = new GameCharacter(
"Goblin", 100, 15, List.of("slash", "dodge")
);
GameCharacter scout = new GameCharacter(goblinTemplate)
.withName("Goblin Scout")
.addSkill("backstab");
GameCharacter weakened = new GameCharacter(goblinTemplate)
.withHealth(50);
System.out.println(goblinTemplate.getHealth()); // 100 ✅
System.out.println(scout.getSkills()); // [slash, dodge, backstab]
System.out.println(weakened.getHealth()); // 50Java Prototype Registry
import java.util.HashMap;
import java.util.Map;
public class CharacterRegistry {
private final Map<String, GameCharacter> prototypes = new HashMap<>();
public void register(String key, GameCharacter prototype) {
prototypes.put(key, prototype);
}
public GameCharacter create(String key) {
GameCharacter prototype = prototypes.get(key);
if (prototype == null) {
throw new IllegalArgumentException("Prototype not found: " + key);
}
return new GameCharacter(prototype); // copy constructor
}
}
public class Game {
public static void main(String[] args) {
CharacterRegistry registry = new CharacterRegistry();
registry.register("goblin",
new GameCharacter("Goblin", 100, 15, List.of("slash")));
registry.register("troll",
new GameCharacter("Troll", 500, 40, List.of("smash", "roar")));
// Spawn wave
List<GameCharacter> wave = new ArrayList<>();
for (int i = 0; i < 20; i++) {
wave.add(registry.create("goblin"));
}
for (int i = 0; i < 5; i++) {
wave.add(registry.create("troll"));
}
System.out.println("Wave spawned: " + wave.size() + " enemies");
}
}JavaScript's Prototypal Inheritance
JavaScript is unique: it uses Prototype at the language level. Every object has a [[Prototype]] link to another object. This is not the GoF Prototype pattern — it's a different concept with the same name.
// JavaScript's built-in prototypal inheritance
const animalProto = {
speak() {
return `${this.name} says ${this.sound}`;
},
eat() {
return `${this.name} is eating`;
}
};
// Object.create() implements prototypal inheritance
const dog = Object.create(animalProto);
dog.name = "Rex";
dog.sound = "Woof";
console.log(dog.speak()); // "Rex says Woof"
console.log(Object.getPrototypeOf(dog) === animalProto); // true
// Modern classes are syntactic sugar over this prototype chain
class Animal {
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
speak() {
return `${this.name} says ${this.sound}`;
}
}
class Dog extends Animal {
constructor(name) {
super(name, "Woof");
}
fetch() { return `${this.name} fetches the ball!`; }
}
// Under the hood, Dog.prototype is linked to Animal.prototype
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // trueGoF Prototype Pattern in JavaScript
To implement the GoF pattern (cloning) in JavaScript:
// Using structuredClone (Node 17+, modern browsers) — deep clone
const template = {
type: "enemy",
stats: { health: 100, attack: 15 },
skills: ["slash", "dodge"],
clone() {
return structuredClone(this);
}
};
const enemy1 = template.clone();
enemy1.stats.health = 80; // wounded
console.log(template.stats.health); // 100 ✅ — structuredClone is a true deep copy
// Older approach: JSON round-trip (works for plain data, loses methods)
const deepCopy = JSON.parse(JSON.stringify(template));Real-World Use Cases
1. Configuration Presets
class ServerConfig {
constructor(
public host: string,
public port: number,
public timeout: number,
public retries: number,
public headers: Record<string, string>,
public features: string[]
) {}
clone(): ServerConfig {
return new ServerConfig(
this.host,
this.port,
this.timeout,
this.retries,
{ ...this.headers },
[...this.features]
);
}
withTimeout(timeout: number): ServerConfig {
const clone = this.clone();
clone.timeout = timeout;
return clone;
}
withFeature(feature: string): ServerConfig {
const clone = this.clone();
clone.features.push(feature);
return clone;
}
}
// Base config
const baseConfig = new ServerConfig(
"api.example.com", 443, 5000, 3,
{ "Content-Type": "application/json" },
["auth", "logging"]
);
// Derived configs
const fastConfig = baseConfig.withTimeout(1000);
const debugConfig = baseConfig.withFeature("verbose-logging").withFeature("request-tracing");
const productionConfig = baseConfig.withTimeout(3000).withFeature("rate-limiting");2. UI Component Templates
interface ComponentConfig {
styles: Record<string, string>;
classes: string[];
attributes: Record<string, string>;
clone(): ComponentConfig;
}
class ButtonConfig implements ComponentConfig {
constructor(
public styles: Record<string, string>,
public classes: string[],
public attributes: Record<string, string>
) {}
clone(): ButtonConfig {
return new ButtonConfig(
{ ...this.styles },
[...this.classes],
{ ...this.attributes }
);
}
}
// Design system base components
const primaryButton = new ButtonConfig(
{ background: "#3b82f6", color: "white", padding: "8px 16px" },
["btn", "btn-primary"],
{ type: "button", role: "button" }
);
// Variants from prototype
const dangerButton = primaryButton.clone();
dangerButton.styles.background = "#ef4444";
dangerButton.classes.push("btn-danger");
const disabledButton = primaryButton.clone();
disabledButton.styles.opacity = "0.5";
disabledButton.attributes.disabled = "true";
disabledButton.attributes["aria-disabled"] = "true";When to Use the Prototype Pattern
Use Prototype when:
- Object creation is expensive (DB queries, network calls, file I/O, heavy computation)
- You need many similar objects that differ only in a few fields
- The class to instantiate is specified at runtime (via registry)
- You want to avoid building complex class hierarchies of factories
- The object has many optional fields and you want configuration presets
Avoid Prototype when:
- Objects are cheap to create — just use
new - All fields must be set fresh — cloning provides no benefit
- Deep copy is complex or has side effects (database connections, file handles, threads)
- You need a completely different type, not a copy of an existing one
Prototype vs Other Creational Patterns
| Pattern | Creates Object Via | Best For |
|---|---|---|
| Factory Method | Subclass decides | Varying types of products |
| Abstract Factory | Family of factories | Related product families |
| Builder | Step-by-step construction | Complex objects with many optional parts |
| Prototype | Cloning existing instance | Copies of expensive-to-create objects |
| Singleton | Single shared instance | Exactly one instance needed |
Prototype and Builder are sometimes combined: build one prototype with Builder, then clone it repeatedly.
Common Pitfalls
1. Accidental Shallow Copy
// ❌ Bug: arrays and objects are shared
class Config {
tags: string[] = [];
clone(): Config {
return Object.assign(new Config(), this); // shallow!
}
}
const base = new Config();
base.tags.push("prod");
const clone = base.clone();
clone.tags.push("debug"); // modifies base.tags too!
console.log(base.tags); // ["prod", "debug"] — oops// ✅ Fix: deep copy all mutable references
class Config {
tags: string[] = [];
clone(): Config {
const c = new Config();
c.tags = [...this.tags]; // copy the array
return c;
}
}2. Circular References in Deep Copy
class Node {
next: Node | null = null;
prev: Node | null = null; // circular!
deepClone(): Node {
// Naive deepClone would cause infinite recursion
// Use a Map to track visited nodes
const visited = new Map<Node, Node>();
function cloneNode(node: Node | null): Node | null {
if (!node) return null;
if (visited.has(node)) return visited.get(node)!;
const cloned = new Node();
visited.set(node, cloned);
cloned.next = cloneNode(node.next);
cloned.prev = cloneNode(node.prev);
return cloned;
}
return cloneNode(this)!;
}
}3. Cloning Objects with Non-Cloneable Resources
class DatabaseConnection {
private connection: pg.Client; // cannot be cloned!
constructor(config: DBConfig) {
this.connection = new pg.Client(config);
}
// ❌ Don't clone this — clone the config, create a new connection
clone(): DatabaseConnection {
// This would share the same underlying socket — dangerous
return Object.assign(new DatabaseConnection({} as DBConfig), this);
}
}
// ✅ Better: prototype stores config, not the live connection
class DBConnectionFactory {
constructor(private config: DBConfig) {}
create(): pg.Client {
return new pg.Client({ ...this.config }); // fresh connection every time
}
}Summary
The Prototype pattern is the most straightforward creational pattern — clone instead of construct. Its power lies in:
- Performance: Avoid expensive initialization by copying a pre-built prototype
- Flexibility: Modify clones freely without affecting the original
- Simplicity: Registry + clone is often simpler than factories with complex hierarchies
Key takeaways:
✅ Always distinguish shallow copy from deep copy — most bugs come from accidentally sharing mutable references
✅ Use a Prototype Registry to manage and retrieve named prototypes by key
✅ In Python, prefer copy.deepcopy() or implement __deepcopy__ for full control
✅ In Java, prefer copy constructors over Cloneable (which is widely considered broken)
✅ In JavaScript, structuredClone() is the modern, correct way to deep clone
✅ Watch out for circular references and non-cloneable resources (file handles, DB connections)
What's Next
Up next in the OOP & Design Patterns series:
- OOP-12: Adapter and Facade Patterns — structural patterns for incompatible interfaces
- OOP-13: Decorator and Proxy Patterns — wrapping objects to extend or control behavior
- OOP-14: Strategy and Template Method — behavioral patterns for algorithms
Practice Exercises
-
Game Spawner: Build a
MonsterRegistrythat holds 5 different monster prototypes (each with health, attack, skills, and loot tables). Implement aspawnWave(type, count)function that clones and positions them on a 100×100 grid. -
Email Template Engine: Create a
EmailTemplateclass with subject, body (with{{placeholder}}variables), and recipient lists. Implementclone()and afill(data: Record<string, string>)method that returns a new email with placeholders replaced. -
Config Builder: Implement a
HttpClientConfigwith base URL, timeout, retry count, and default headers. Build three preset configs (fast, standard, resilient) derived from a base prototype, each with different timeout/retry values. -
Circular List Clone: Implement a doubly linked circular list (
head.prev = tail,tail.next = head) and write adeepClone()that correctly handles the circular references without infinite recursion.
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.