Symptom

A factory class has one createX method per variant, and each method repeats the same setup steps with only a few field values differing. Adding a new variant means writing yet another near-identical method, or copying one and forgetting to change a field. The reader's per visit rises as the method count grows.

Goal

Variants live as configured prototype instances in a registry; creation is clone + tweak. Adding a new variant is a registry entry, not a new method on the factory. Defaults are inherited from the prototype; per-instance overrides are explicit, supporting between variant shape and creation logic.

Before the pattern

class GraphicEditor {
createCircle(x, y) {
const c = new Shape();
c.kind = 'circle';
c.fill = '#3498db';
c.stroke = '#2c3e50';
c.strokeWidth = 2;
c.x = x; c.y = y; c.r = 20;
return c;
}
createSquare(x, y) {
const s = new Shape();
s.kind = 'square';
s.fill = '#e74c3c';
s.stroke = '#2c3e50';
s.strokeWidth = 2;
s.x = x; s.y = y; s.side = 30;
return s;
}
createStar(x, y) {
const s = new Shape();
s.kind = 'star';
s.fill = '#f1c40f';
s.stroke = '#2c3e50';
s.strokeWidth = 2;
s.x = x; s.y = y; s.points = 5; s.r = 25;
return s;
}
}

After the pattern

class Shape {
clone() {
return Object.assign(new Shape(), this);
}
}
const SHAPE_PROTOTYPES = {
circle: Object.assign(new Shape(), { kind: 'circle', fill: '#3498db', stroke: '#2c3e50', strokeWidth: 2, r: 20 }),
square: Object.assign(new Shape(), { kind: 'square', fill: '#e74c3c', stroke: '#2c3e50', strokeWidth: 2, side: 30 }),
star: Object.assign(new Shape(), { kind: 'star', fill: '#f1c40f', stroke: '#2c3e50', strokeWidth: 2, points: 5, r: 25 }),
};
function createShape(kind, x, y) {
const shape = SHAPE_PROTOTYPES[kind].clone();
shape.x = x;
shape.y = y;
return shape;
}
Example source: Illustrative example written for this site in the spirit of Design Patterns (Gamma, Helm, Johnson, Vlissides, Addison-Wesley, 1994), chapter 3. The book's running example is a graphic editor's shape palette; this JavaScript adaptation keeps the registry-of-prototypes shape so adding a new shape variant is data, not a new createX method.
Pressure

Per-variant create methods are Duplicated Code at the worst level — the shared structure is implicit in convention, not enforced in code. A field added to the shared shape requires editing every create method; one missed method silently produces an inconsistent variant. The team's compounds across methods that are not test-paired.

Tradeoff

Cloning is shallow by default — nested mutable references shared across instances cause action-at-a-distance bugs that are hard to localize. Deep clone semantics must be designed per type; getting them wrong on a frequently-cloned prototype produces subtle aliasing bugs that survive code review. The team's on aliasing bugs scales with the clone graph's depth.

Relief

Variants become data — readable as a table of (kind, defaults) pairs, easy to extend, easy to audit for consistency. New variants are PR-sized; the factory function shrinks to one line per request. The team's drops because new variants add data, not code.

Trap

When variants stop sharing a coherent shape, the prototype registry hides the divergence in optional fields and conditional cloning. Stop and reach for a real type hierarchy when 'shape' variants need different fields or behaviour. The cure adds that raises per read as variant-specific exceptions accrete.