Symptom

A type that should have many instances (every glyph in a document, every tile on a map, every projectile in a scene) carries enough per-instance state that memory scales linearly with instance count. Most of that state — texture, mesh, font metrics — is identical across instances of the same kind.

Goal

Intrinsic state (the kind-shared data) lives in one Flyweight object per kind; extrinsic state (the per-instance data: position, scale, custom parameters) lives on the instance. A registry hands out the shared Flyweight; instances reference it rather than copy it.

Before the pattern

class Tree {
constructor(species, x, y, age, scale) {
this.species = species;
this.texture = loadTexture(species);
this.mesh = loadMesh(species);
this.animation = loadAnimation(species);
this.x = x;
this.y = y;
this.age = age;
this.scale = scale;
}
render(ctx) {
/* uses this.texture, this.mesh, this.x, this.y */
}
}
const forest = [];
for (let i = 0; i < 10000; i++) {
forest.push(new Tree('oak', randomX(), randomY(), randomAge(), 1.0));
}
// 10000 × 7MB of texture/mesh data per tree = catastrophic memory cost.

After the pattern

class TreeType {
constructor(species) {
this.species = species;
this.texture = loadTexture(species);
this.mesh = loadMesh(species);
this.animation = loadAnimation(species);
}
render(ctx, x, y, age, scale) {
/* uses this.texture, this.mesh, x, y, scale */
}
}
class TreeTypeRegistry {
static cache = new Map();
static get(species) {
if (!TreeTypeRegistry.cache.has(species)) {
TreeTypeRegistry.cache.set(species, new TreeType(species));
}
return TreeTypeRegistry.cache.get(species);
}
}
class Tree {
constructor(species, x, y, age, scale) {
this.type = TreeTypeRegistry.get(species);
this.x = x;
this.y = y;
this.age = age;
this.scale = scale;
}
render(ctx) {
this.type.render(ctx, this.x, this.y, this.age, this.scale);
}
}
// 10000 Trees × ~40 bytes each + one shared TreeType per species.
Example source: Illustrative example written for this site in the spirit of Design Patterns (Gamma, Helm, Johnson, Vlissides, Addison-Wesley, 1994), chapter 4. The book's running example is a text editor's per-character glyph objects; this JavaScript adaptation uses a forest of game-engine trees because the texture/mesh-sharing payoff (and the intrinsic/extrinsic split) reads more concretely in modern terms.
Pressure

Per-instance copies of shared state are Duplicated Code in memory form — N×M bytes when 1×M would suffice, where N is instance count and M is intrinsic-state size. The cost is invisible during development on small datasets and catastrophic when the dataset reaches realistic scale.

Tradeoff

Splitting intrinsic from extrinsic state forces every method on the instance to receive the extrinsic state explicitly (as args or as `this.x`-style fields) rather than reading it from intrinsic-side fields. Methods on the Flyweight take more parameters; client code at every call site provides them.

Relief

Memory cost drops from N×M to N + M; load time drops because each kind loads once; instance construction becomes a registry lookup + per-instance field set. Tests of the intrinsic state run once per kind, not per instance.

Trap

Flyweight applied where extrinsic state is mutable and the instance count is small adds indirection without paying for itself. The agent reads `tree.type.render(ctx, this.x, ...)` and must follow two hops to understand what render actually does, costing context budget that the memory savings can't justify at small N.