Prototype
Specifies the kinds of objects to create using a prototypical
instance and creates new objects by cloning that
prototype — avoiding instantiation via new and allowing
variants to be created from a pre-configured base state.
Intent
Create new objects by cloning an existing instance (the
prototype) instead of instantiating via new. The client
code asks the prototype to clone itself — without needing to know the
concrete class of the object being created.
Cataloged by Gamma, Helm, Johnson and Vlissides in the book Design Patterns: Elements of Reusable Object-Oriented Software (1994), Prototype belongs to the creational patterns category. It's useful when building an object from scratch is costly or complex, when you want to create variations from a pre-configured base state, or when the concrete class is only known at runtime.
Problem
Imagine a system that needs to create variations of a service configuration. Each variant (staging, production, load testing) shares a large set of base parameters and differs only in a few fields. Building each variant from scratch — repeating every common parameter — is tedious, error-prone and violates the DRY principle.
A naive solution is to copy and paste the instantiation, but that scatters parameters across the code. Another is a Factory Method for each variant, but if the variants are configured dynamically at runtime, the number of factories can explode.
Prototype solves this: you create a fully configured base object (the prototype), register it in a catalog, and, when you need a variation, clone the closest prototype and adjust only what differs.
Prototype vs Factory
- Factory Method / Abstract Factory create objects from scratch, instantiating the concrete classes on every call. Configuration logic lives in the factory.
- Prototype creates objects by copying an existing, already-configured instance. Configuration logic lives in the prototype's state — useful when creation involves accumulated state that would be hard to reproduce every time.
Solution
Prototype organizes the code into three participants:
-
Prototype (interface): declares the cloning method,
usually called
clone(). Every cloneable class implements this interface. -
ConcretePrototype: implements the
clone()method, creating a copy of itself. It's the ConcretePrototype's responsibility to guarantee that the copy is deep when necessary — this is the pattern's most critical point. - Prototype Registry (optional): a catalog of pre-configured prototypes. The client requests a clone by name, without needing to know which concrete class is behind it. Replaces Factory Method when the types are configured at runtime.
The most critical point in the implementation is the distinction between shallow copy and deep copy. A shallow copy only copies references to nested objects — the clone and the original end up sharing the same sub-objects, and modifying one affects the other. A deep copy independently recreates every nested object.
Structure
«interface»
Cloneable
┌──────────────────────┐
│ + clone(): Cloneable │
└──────────────────────┘
▲
┌─────────┴──────────────────────┐
│ │
ConfigTemplate ConfigPremium
(ConcretePrototype) (ConcretePrototype)
clone() clone()
→ new ConfigTemplate(...) → new ConfigPremium(...)
// deep copy of fields // deep copy of fields
PrototypeRegistry
┌─────────────────────────────────────────────────┐
│ - catalog: Map<string, Cloneable> │
│ + register(key: string, proto: Cloneable): void │
│ + create(key: string): Cloneable │
└─────────────────────────────────────────────────┘
│
│ catalog.get(key).clone()
▼
new independent object (clone of the prototype)
Flow:
registry.register("production", baseConfig)
const serv = registry.create("production") // returns a clone
serv.name = "payments" // does not affect the prototype
Code examples
Example 1 — Shallow copy vs Deep copy
Prototype's central pitfall: a shallow copy shares references to
nested objects between the clone and the original, causing unexpected
side effects. The clone() method must guarantee a deep
copy of every property that is a reference.
// In TypeScript, arrays and objects are passed by reference.
// A shallow copy only copies the pointer — clone and original share
// the same underlying object/array.
class ConfigTemplate {
constructor(
public name: string,
public timeoutMs: number,
public tags: string[], // array — shared reference on shallow copy
public headers: Record<string, string>, // object — shared reference on shallow copy
) {}
// ── WRONG: shallow copy via Object.assign ────────────────
// Object.assign only copies the first level: tags and headers still
// point to the SAME objects as the original instance.
cloneShallow(): ConfigTemplate {
return Object.assign(
new ConfigTemplate("", 0, [], {}),
this,
);
}
// ── Sufficient copy: 1 level is enough because the values are primitives ──
// tags: array of strings (primitives) → spread guarantees full independence.
// headers: object whose values are strings (primitives) → 1-level spread is safe.
// If the values were nested objects, they would need to be cloned recursively.
clone(): ConfigTemplate {
return new ConfigTemplate(
this.name,
this.timeoutMs,
[...this.tags], // new independent array
{ ...this.headers }, // new independent object (values are primitives)
);
}
}
// ── Demonstrating the problem ─────────────────────────────────
const base = new ConfigTemplate(
"base-service",
5000,
["production", "critical"],
{ "X-Api-Key": "base-key" },
);
// Shallow copy — DANGEROUS:
const shallow = base.cloneShallow();
shallow.tags.push("debug"); // modifies the BASE array!
shallow.headers["X-Debug"] = "true"; // modifies the BASE object!
console.log(base.tags); // ["production", "critical", "debug"] ← contaminated
console.log("X-Debug" in base.headers); // true ← contaminated
// Deep copy — CORRECT:
const deep = base.clone();
deep.name = "staging-service";
deep.tags.push("verbose");
deep.headers["X-Env"] = "staging";
console.log(base.name); // "base-service" ← unchanged
console.log(base.tags); // ["production", "critical", "debug"] (from the shallow copy above)
console.log("X-Env" in base.headers); // false ← deep clone did not contaminate
<?php
// In PHP, arrays are COPIED BY VALUE — `clone` already duplicates them
// automatically. The danger lies in properties that are OBJECTS: without
// __clone(), the clone and the original share the same nested object instance.
class ConnectionConfig
{
public function __construct(
public string $host,
public int $port,
) {}
}
class ConfigTemplate
{
/** @param string[] $tags */
public function __construct(
public string $name,
public int $timeoutMs,
public array $tags, // array — PHP copies by VALUE on clone: safe
public ConnectionConfig $connection, // object — needs explicit cloning
) {}
// __clone() is automatically invoked by the `clone` operator.
// Without it, $this->connection would be the SAME instance as the original.
public function __clone(): void
{
// $tags: array is copied by value — no action needed.
// $connection: object needs to be cloned to guarantee independence.
$this->connection = clone $this->connection;
}
}
// ── Demonstrating the problem WITHOUT __clone ─────────────────
class ConfigWithoutDeepClone
{
public function __construct(
public string $name,
public ConnectionConfig $connection,
) {}
// Without __clone: the `clone` operator does not descend into object properties.
}
$baseWithout = new ConfigWithoutDeepClone('base', new ConnectionConfig('localhost', 5432));
$shallowCopy = clone $baseWithout;
$shallowCopy->connection->host = 'production.db'; // modifies the ORIGINAL!
echo $baseWithout->connection->host . PHP_EOL; // "production.db" ← contaminated!
// ── Demonstrating WITH __clone (ConfigTemplate) ────────────────
$base = new ConfigTemplate(
'base-service',
5000,
['production', 'critical'],
new ConnectionConfig('localhost', 5432),
);
$copy = clone $base; // __clone() is called automatically
$copy->name = 'staging-service';
$copy->tags[] = 'verbose'; // array: copied by value — safe
$copy->connection->host = 'staging.db'; // object: deep copy — safe
echo $base->name . PHP_EOL; // "base-service" ← unchanged
echo implode(', ', $base->tags) . PHP_EOL; // "production, critical" ← unchanged
echo $base->connection->host . PHP_EOL; // "localhost" ← unchanged
Example 2 — Prototype Registry
The Registry keeps a catalog of pre-configured prototypes. The client requests a clone by name — without knowing the concrete class behind it. It works like a Factory that creates by cloning instead of instantiating.
// ── Prototype Registry ────────────────────────────────────────
// Catalog of pre-configured prototypes.
// The client always receives a CLONE — never the original prototype.
class PrototypeRegistry {
private readonly catalog = new Map<string, ConfigTemplate>();
register(key: string, proto: ConfigTemplate): void {
this.catalog.set(key, proto);
}
create(key: string): ConfigTemplate {
const proto = this.catalog.get(key);
if (!proto) {
throw new Error(`Prototype not found: "${key}".`);
}
return proto.clone(); // returns a CLONE — original prototype is never exposed
}
}
// ── Registering pre-configured prototypes ─────────────────────
const registry = new PrototypeRegistry();
registry.register("production", new ConfigTemplate(
"production",
3000,
["critical", "monitored"],
{ "X-Api-Key": "prod-key", "X-Env": "prod" },
));
registry.register("staging", new ConfigTemplate(
"staging",
10000,
["debug", "verbose"],
{ "X-Api-Key": "stg-key", "X-Env": "staging" },
));
// ── Creating variations by cloning ────────────────────────────
const servA = registry.create("production");
servA.name = "payments"; // customizes the clone, not the prototype
const servB = registry.create("production"); // another independent clone
servB.name = "users";
servB.tags.push("audit");
console.log(servA.name); // "payments"
console.log(servB.name); // "users"
console.log(servA.tags.join(", ")); // "critical, monitored"
console.log(servB.tags.join(", ")); // "critical, monitored, audit"
// Both clones are independent of each other and of the original prototype.
<?php
// ── Prototype Registry ────────────────────────────────────────
class PrototypeRegistry
{
/** @var array<string, ConfigTemplate> */
private array $catalog = [];
public function register(string $key, ConfigTemplate $proto): void
{
$this->catalog[$key] = $proto;
}
// Always returns a CLONE — the original prototype stays intact.
public function create(string $key): ConfigTemplate
{
if (!isset($this->catalog[$key])) {
throw new \InvalidArgumentException(
"Prototype not found: \"{$key}\"."
);
}
return clone $this->catalog[$key]; // __clone() guarantees deep copy
}
}
// ── Registering pre-configured prototypes ─────────────────────
$registry = new PrototypeRegistry();
$registry->register('production', new ConfigTemplate(
'production',
3000,
['critical', 'monitored'],
new ConnectionConfig('db.prod.example.com', 5432),
));
$registry->register('staging', new ConfigTemplate(
'staging',
10000,
['debug', 'verbose'],
new ConnectionConfig('db.staging.example.com', 5432),
));
// ── Creating variations by cloning ────────────────────────────
$servA = $registry->create('production');
$servA->name = 'payments'; // customizes the clone, not the prototype
$servB = $registry->create('production'); // another independent clone
$servB->name = 'users';
$servB->tags[] = 'audit';
echo $servA->name . PHP_EOL; // "payments"
echo $servB->name . PHP_EOL; // "users"
echo implode(', ', $servA->tags) . PHP_EOL; // "critical, monitored"
echo implode(', ', $servB->tags) . PHP_EOL; // "critical, monitored, audit"
// Both clones are independent of each other and of the original prototype.
When to use
- When building from scratch is costly or complex: if initializing an object involves database queries, file reads or heavy computations, cloning an already-ready prototype is more efficient.
- When you need variations from a base state: the prototype carries the common configuration; the clone receives only that variant's specific adjustments.
-
When the concrete class isn't known at compile time:
the client calls
clone()on the interface — without needing to know the concrete class of the object being copied. - As an alternative to Factory Method for runtime-configured types: the Prototype Registry replaces a hierarchy of factories when the types are defined by configuration, not by code.
When to avoid
-
When deep copying is too complex: objects with
circular reference graphs or deep hierarchies make correctly
implementing
clone()very hard to maintain. - When direct instantiation is simple: if creating the object from scratch is trivial and fast, cloning adds complexity without benefit.
- When objects have state that shouldn't be copied: database connections, file descriptors and OS handles don't make sense to clone — cloning an object with these resources can cause undefined behavior.
Pros and cons
Pros
- Creates objects without coupling the client to the concrete class — the client only knows the
Cloneableinterface. - Eliminates repeated initialization code when many objects share the same base configuration.
- Allows creating new objects with complex pre-built state that would be hard to replicate via a constructor.
- The Prototype Registry works as a runtime-configurable factory — new types can be registered without recompiling.
Cons
- Implementing
clone()correctly (deep copy) can be hard for objects with complex hierarchies or circular references. - In PHP, every class that needs deep copying must explicitly implement
__clone()— easy to forget in nested classes. - Cloning objects with external resources (connections, handles) can cause unexpected behavior if those resources aren't handled in the clone.
Common pitfalls
1. Shallow copy — the central pitfall
Warning: This is Prototype's most frequent and most
subtle pitfall. A shallow copy only copies first-level references —
nested objects and arrays continue being shared between the clone and
the original. Modifying the tags array in the clone, for
example, changes the prototype. Always implement clone()
with a deep copy for every property that is a reference.
In TypeScript: use spread ({ ...obj }, [...arr])
for one level when the values are primitives — the most common and
safest case. For arbitrarily deep sub-graphs of pure data
(with no class instances), structuredClone() is a
convenient option, but with important caveats: it preserves cycles and
depth, but discards the prototype chain — the result
isn't an instance of the original class, losing methods like
clone(). It also throws DataCloneError if the
graph contains functions or non-cloneable objects (like a Map
with class-instance values). To clone instances while preserving the
class, do the deep copy field by field inside clone()
itself, using structuredClone() only on pure-data
sub-graphs that contain no class instances.
In PHP: implement __clone() and explicitly clone every
object property — remember that __clone() must be
implemented in every class in the hierarchy that has
sub-objects.
2. Circular references in deep copy
When the object graph contains cycles (A references B which references
A), a naive recursive deep copy loops forever. The solution is to keep
a map of already-visited objects during cloning: if the object has
already been cloned, return the existing clone instead of recreating
it. Note that, although structuredClone() handles cycles
in pure data, it doesn't work for graphs of class instances
(it throws DataCloneError and loses the prototype). For
those cases, implement cycle control manually inside clone(),
passing a Map of already-visited objects as an auxiliary
parameter.
3. Cloning objects with external resources
Database connections, sockets, file descriptors and mutexes shouldn't be
cloned. If your object holds these resources, the __clone()
(PHP) or the clone() method (TypeScript) should close the
resource in the clone and create a new one — or throw an exception
stating the object isn't cloneable.
4. Confusing Prototype with accidental state copying
Prototype is an intentional pattern: you create a prototype on purpose to serve as a base for clones. If you're cloning objects just to avoid writing the constructor, reconsider — a Builder with default values may be more explicit and safer, since it centralizes creation logic without the risk of leaking state from the prototype.
Related patterns
Prototype connects directly to the other creational patterns:
Abstract Factory can use Prototype internally: instead of creating products from scratch, the concrete factory clones registered prototypes — reducing the number of subclasses needed. Builder and Prototype are alternatives for creating complex objects with a pre-defined initial state: Builder assembles the object step by step with centralized validation; Prototype copies an already-assembled, ready object. Choose Builder when the assembly process matters; choose Prototype when the initial state already exists and needs to be replicated with variations.