Creational Pattern (GoF)

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:

  1. Prototype (interface): declares the cloning method, usually called clone(). Every cloneable class implements this interface.
  2. 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.
  3. 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

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.

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 Cloneable interface.
  • 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.