StackPractices
intermediate By Mathias Paulenko

Prototype Pattern for Object Cloning and Configuration

Create new objects by copying existing ones, allowing pre-configured templates and avoiding subclass explosion when object creation is expensive

Topics: design

The Prototype pattern creates new objects by copying existing ones. Instead of building objects from scratch with constructors, you clone a prototype and optionally customize it. This is capable when object initialization is expensive, when many similar configurations exist, or when the exact type of object is not known until runtime.

When to Use This

  • Object creation is costly (database connections, parsed configurations). See Factory Pattern for creation patterns.
  • Many similar object variants exist that differ only slightly. See Builder Pattern for configurable objects.
  • The concrete class to instantiate is determined at runtime. See Strategy Pattern for runtime selection.

Problem

A game spawns hundreds of enemy units with the same base stats but slight variations. Creating each unit from scratch requires reloading assets and parsing configurations repeatedly.

Solution

// prototype/Cloneable.ts
interface Cloneable<T> {
  clone(): T;
}

class EnemyUnit implements Cloneable<EnemyUnit> {
  private health: number;
  private speed: number;
  private weapon: string;
  private abilities: string[];

  constructor(
    health: number,
    speed: number,
    weapon: string,
    abilities: string[]
  ) {
    this.health = health;
    this.speed = speed;
    this.weapon = weapon;
    // Deep copy to prevent shared mutable state
    this.abilities = [...abilities];
  }

  clone(): EnemyUnit {
    return new EnemyUnit(
      this.health,
      this.speed,
      this.weapon,
      [...this.abilities]
    );
  }

  setHealth(health: number): EnemyUnit {
    this.health = health;
    return this;
  }

  addAbility(ability: string): EnemyUnit {
    this.abilities.push(ability);
    return this;
  }

  describe(): string {
    return `${this.health}HP, ${this.speed}SPD, ${this.weapon}, [${this.abilities.join(', ')}]`;
  }
}

// Pre-configured prototypes
const goblinPrototype = new EnemyUnit(30, 8, 'dagger', ['sneak']);
const orcPrototype = new EnemyUnit(80, 4, 'axe', ['rage', 'charge']);

// Clone and customize
const goblinScout = goblinPrototype.clone().setHealth(25).addAbility('scout');
const goblinBoss = goblinPrototype.clone().setHealth(60).addAbility('command');
const orcBerserker = orcPrototype.clone().setHealth(100);

console.log(goblinScout.describe());
console.log(goblinBoss.describe());
console.log(orcBerserker.describe());

Variation: Configuration Template Registry

// prototype/TemplateRegistry.ts
class DocumentTemplate implements Cloneable<DocumentTemplate> {
  private content = '';
  private styles: Record<string, string> = {};
  private metadata: Record<string, unknown> = {};

  constructor() {}

  setContent(content: string): DocumentTemplate {
    this.content = content;
    return this;
  }

  setStyles(styles: Record<string, string>): DocumentTemplate {
    this.styles = { ...styles };
    return this;
  }

  setMetadata(metadata: Record<string, unknown>): DocumentTemplate {
    this.metadata = { ...metadata };
    return this;
  }

  clone(): DocumentTemplate {
    return new DocumentTemplate()
      .setContent(this.content)
      .setStyles({ ...this.styles })
      .setMetadata({ ...this.metadata });
  }
}

class TemplateRegistry {
  private templates = new Map<string, DocumentTemplate>();

  register(name: string, template: DocumentTemplate): void {
    this.templates.set(name, template);
  }

  create(name: string): DocumentTemplate {
    const template = this.templates.get(name);
    if (!template) throw new Error(`Unknown template: ${name}`);
    return template.clone();
  }
}

// Usage
const registry = new TemplateRegistry();
registry.register('report', new DocumentTemplate()
  .setContent('## Report\n\nDate: {{date}}')
  .setStyles({ font: 'Arial', size: '12pt' }));

registry.register('invoice', new DocumentTemplate()
  .setContent('## Invoice #{{id}}\n\nTotal: {{total}}')
  .setStyles({ font: 'Times', size: '10pt' }));

const report = registry.create('report');

How It Works

  1. Prototype declares a clone method
  2. Concrete Prototype implements deep cloning to avoid shared mutable state
  3. Client clones prototypes and optionally customizes the copy
  4. Registry (optional) holds named prototypes for convenient access

Production Considerations

  • Always deep-clone nested objects and arrays to prevent accidental sharing
  • Use structuredClone in modern JavaScript for deep copies of plain objects
  • For circular references, implement custom cloning logic

Common Mistakes

  • Shallow cloning mutable nested state, causing side effects across instances
  • Not implementing clone in subclasses, breaking the pattern chain
  • Using Prototype when a simple constructor or Factory Method is cleaner

Troubleshooting

  • Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
  • Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
  • Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
  • Tests break when the design changes: favor stable contracts over internal structure.
  • Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.

Quick Reference

  • Main command: run the base solution from the article and verify the expected result.
  • Validation: confirm tests pass and key metrics did not degrade.
  • Rollback: if something fails, revert the change and consult the Troubleshooting section.

Further Reading

  • Official documentation: check the current reference for the framework or tool used.
  • Related guides: explore the prototype and creational-patterns guides for deeper coverage.
  • Complementary patterns: review design patterns applicable to your technology stack.
  • Public postmortems: study real incidents from teams that faced similar production issues.

Production Notes

  • Deploy gradually using canary or blue-green to catch regressions early.
  • Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
  • Document the rollback in the runbook; test the procedure in staging at least once per quarter.
  • Review structured logs with correlation IDs to trace requests end-to-end during incidents.

Key Takeaways

  • Apply prototype pattern for object cloning and configuration when you need a practical solution for your use case.
  • Monitor performance after implementation; measure latency, errors, and resource usage before and after.
  • Check the Troubleshooting section for common failures; most have documented root causes with fixes.
  • Keep dependencies updated and run tests in CI to prevent production regressions.

Advanced Topics

Scenario: Configuration Cloning for Multi-tenant

// Prototype pattern for tenant configuration
interface TenantConfig {
  theme: Theme;
  features: string[];
  limits: ResourceLimits;
  clone(): TenantConfig;
}

class DefaultTenantConfig implements TenantConfig {
  constructor(
    public theme: Theme,
    public features: string[],
    public limits: ResourceLimits
  ) {}

  clone(): TenantConfig {
    // Deep clone: recursive copy of nested objects
    return new DefaultTenantConfig(
      { ...this.theme },
      [...this.features],
      { ...this.limits }
    );
  }
}

// Usage: create new tenant from base config
const baseConfig = new DefaultTenantConfig(
  { primary: "#3b82f6", mode: "light" },
  ["auth", "dashboard", "reports"],
  { maxUsers: 100, maxStorage: 10 }
);

// Clone and customize for premium client
const premiumConfig = baseConfig.clone();
premiumConfig.features.push("sso", "audit-log");
premiumConfig.limits.maxUsers = 1000;
premiumConfig.limits.maxStorage = 100;

// Clone for basic client
const basicConfig = baseConfig.clone();
basicConfig.features = ["auth", "dashboard"];
basicConfig.limits.maxUsers = 10;
basicConfig.limits.maxStorage = 1;

// Comparison: deep clone vs structuredClone vs JSON
  | Method | Advantages | Disadvantages |
  |--------|------------|---------------|
  | Manual (spread) | Full control | Tedious for deep objects |
  | JSON.parse(JSON.stringify) | Simple | No Date, Map, functions |
  | structuredClone() | Native, supports Date/Map | No functions |
  | lodash _.cloneDeep | Robust | External dependency |

Lessons:

  • Prototype clones objects without coupling to concrete class
  • Deep clone is necessary for nested objects
  • structuredClone() is native in Node.js 17+ and modern browsers
  • For multi-tenant config, clone base and customize
  • Avoid JSON.parse/stringify: loses types (Date, Map, undefined)

### When do I use structuredClone vs manual clone?

Use structuredClone() when you need deep clone of plain objects with native types (Date, Map, Set, ArrayBuffer). Use manual clone when you need control over what to clone (e.g: do not clone secrets, reuse shared references). Use lodash _.cloneDeep for complex cases with circular references. Avoid JSON.parse/stringify: loses Date, Map, Set, undefined and functions.






















End of document. Review and update quarterly.

## Common Production Pitfalls

- Applying the pattern where no abstraction is needed, adding accidental complexity.
- Letting the pattern leak into unrelated modules and blur ownership boundaries.
- Over-engineering the first implementation instead of starting simple and measuring pain.
- Skipping contract tests, so refactors silently break consumers.
- Ignoring failure modes that the pattern does not cover.
- Using the pattern as a default instead of choosing the right tool for the current scale.
- Forgetting to document when to stop using the pattern and what replaces it.
- Missing observability around the pattern's performance and error propagation.

Frequently Asked Questions

How is this different from Factory Method?

Factory Method creates objects through a factory class. Prototype creates objects by copying an existing instance, preserving its state.

Can I use this with JSON?

Yes. JSON.parse(JSON.stringify(obj)) is a crude prototype clone for plain objects, but structuredClone is preferred for modern runtimes.