Flyweight
Intermediate · 11 min read · ▶ live playground · ✦ checkpoint
A forest editor feels fast with one hundred trees, then runs out of memory at one million. Each tree stores a position and color, but it also carries another copy of the same oak mesh and bark texture.
The Flyweight pattern shares fine-grained objects so a large population can fit in memory. A fine-grained object represents one small item in that population; the pattern earns its complexity when many such items repeat the same expensive data.
Sharing reduces allocation and memory use, but it splits one object's state across two places and adds cache lookups. That trade only pays when measurement shows enough repetition at a large enough scale.
Before: every tree duplicates heavyweight assets
The position and color differ for each tree. The mesh and texture do not, yet this constructor loads fresh copies for every instance:
function loadMesh(species: string): Float32Array {
return new Float32Array(species === "oak" ? 50_000 : 40_000);
}
function loadTexture(species: string): Uint8Array {
return new Uint8Array(species === "oak" ? 80_000 : 60_000);
}
class Tree {
private x: number;
private y: number;
private color: string;
private mesh: Float32Array;
private texture: Uint8Array;
public constructor(
species: string,
x: number,
y: number,
color: string,
) {
this.x = x;
this.y = y;
this.color = color;
// smell: identical heavyweight assets are allocated per tree.
this.mesh = loadMesh(species);
this.texture = loadTexture(species);
}
}One tree object is not alarming. Multiply both arrays by hundreds of thousands of oaks and the repeated asset data dominates the small values that actually vary.
Moving every field into one global object would be wrong too: each tree still needs its own location and color. Flyweight starts by separating the two kinds of state.
After: share intrinsic state, pass extrinsic state
Intrinsic state is context-free data that can be shared safely, such as an oak's mesh and texture. Extrinsic state is data that belongs to one context, such as a tree's position and color; the context passes it to the shared object when work happens.
A flyweight factory creates and caches flyweights by a stable key. Repeated requests for the same key return the same instance rather than rebuilding its intrinsic state.
class diagram
Trees share one immutable TreeType per species
- 1Flyweight receives context-specific values instead of storing one copy for every tree.
- 2TreeType implements the contract and keeps one immutable mesh and texture for a species.
- 3FlyweightFactory owns the cache and returns an existing flyweight for a repeated key.
- 4Tree keeps extrinsic position and color, then supplies them when it uses the shared TreeType.
TreeType is the concrete flyweight. Tree is the small context object, the object that holds one item's extrinsic state and refers to shared intrinsic state. The factory's cache is the point that prevents duplicate TreeType instances.
interface Flyweight {
draw(x: number, y: number, color: string): string;
}
class TreeType implements Flyweight {
private readonly species: string;
private readonly mesh: readonly number[];
private readonly texture: readonly number[];
public constructor(
species: string,
mesh: readonly number[],
texture: readonly number[],
) {
this.species = species;
this.mesh = mesh;
this.texture = texture;
}
public draw(x: number, y: number, color: string): string {
return (
`${this.species} at (${x}, ${y}), ${color}` +
` [mesh ${this.mesh.length}, texture ${this.texture.length}]`
);
}
}
class FlyweightFactory {
private cache = new Map<string, TreeType>();
public get(species: string): TreeType {
const cached = this.cache.get(species);
if (cached !== undefined) {
return cached;
}
const created = new TreeType(
species,
Object.freeze([0, 1, 2]),
Object.freeze([12, 24, 48, 96]),
);
this.cache.set(species, created);
return created;
}
public get count(): number {
return this.cache.size;
}
}
class Tree {
private readonly x: number;
private readonly y: number;
private readonly color: string;
private readonly type: TreeType;
public constructor(
x: number,
y: number,
color: string,
type: TreeType,
) {
this.x = x;
this.y = y;
this.color = color;
this.type = type;
}
public render(): string {
return this.type.draw(this.x, this.y, this.color);
}
}
const factory = new FlyweightFactory();
const firstOakType = factory.get("oak");
const secondOakType = factory.get("oak");
const greenOak = new Tree(10, 20, "green", firstOakType);
const amberOak = new Tree(80, 20, "amber", secondOakType);
console.log(`same TreeType: ${firstOakType === secondOakType}`);
console.log(`cached TreeTypes: ${factory.count}`);
console.log(greenOak.render());
console.log(amberOak.render());same TreeType: true cached TreeTypes: 1 oak at (10, 20), green [mesh 3, texture 4] oak at (80, 20), amber [mesh 3, texture 4]
Both contexts hold different coordinates and colors, but the identity comparison proves that their TreeType reference is the same object. A million Tree contexts can therefore reuse one oak mesh and texture instead of loading a million copies.
The factory key defines what “the same” means. A real renderer might key by species, mesh version, texture, and quality level. Leaving a relevant intrinsic property out of the key can return the wrong shared object; putting position into the key would destroy most sharing.
What the factory saves—and what it costs
The factory pays a lookup on each request and retains cached objects. If keys come from unbounded user input, the cache itself can grow without limit. A production design may need eviction, a bounded key space, or ownership tied to one document or scene.
The split also makes method signatures wider. TreeType.draw() now needs position and color because it cannot read them from shared intrinsic state. That is the cost of making one object usable across many contexts.
The numbers decide whether the trade is worthwhile. Estimate the bytes in one duplicated flyweight, count how many instances repeat it, and measure the lookup and coordination overhead. “There may be many later” is weaker evidence than a memory profile.
When NOT to use it
Flyweight rarely helps a collection of fifty cheap objects. A cache, key scheme, factory, and state split can mangle a clean model while saving an amount of memory nobody can measure. Keep the direct object until scale and repetition are demonstrated.
Do not share state that must vary independently. Making TreeType.color mutable so one tree can turn amber would recolor every context that shares it. Mutable intrinsic state is a bug factory because an apparently local edit has population-wide effects.
The pattern is also a poor fit when identifying and passing extrinsic state costs more than the objects being saved. Its benefit is shared heavyweight data, not a lower class count: Flyweight introduces more roles in exchange for fewer duplicate instances.
Checkpoint
Answer all three to mark this lesson complete
Flyweight controls how many shared objects are created. Next, Proxy controls when and under what conditions a client may reach the real object behind the same interface.