Intermediate12 min readDesign Patternslive prototype

Flyweight

Share the heavy, repeated part of many objects so huge counts become cheap. A game forest needs 10,000 trees, but there are only 3 tree *species* — so instead of every tree carrying its own 2 KB copy of sprite data, all 10,000 trees point at 3 shared, immutable `TreeType` objects and keep only their own tiny `x, y`. Memory drops from ~20 MB to ~0.4 MB.

The idea

What it is

Flyweight answers one question: how do you have tons of objects without running out of memory? The trick is noticing that most of those objects are secretly carrying the same heavy data. Picture a game that must render a forest of 10,000 trees. Each tree needs sprite, texture and mesh data — say 2 KB of it. But look closer: there are only 3 tree species (Oak, Pine, Palm). The naive version gives every single tree its own copy of that 2 KB, so 10,000 trees cost roughly 20 MB for data that is 99.97% duplicated.

Flyweight splits each tree in two. The heavy part that repeats — the sprite, the color, the texture — moves into 3 shared, immutable `TreeType` objects, built once and cached in a factory. Each of the 10,000 trees then keeps only what is truly its own: an x, a y, and a reference to its shared TreeType. That's about 40 bytes per tree. Total: 3 × 2 KB of heavy data plus 10,000 tiny records — around 0.4 MB. The forest looks identical on screen; only the memory bill changed.

The one sentence to remember

Flyweight shares the heavy, repeated part of many objects (in immutable, cached flyweights) so that having a huge number of objects becomes cheap — each object keeps only its tiny unique bits plus a reference to the shared part.

You've already used flyweights today

Your text editor doesn't create a fresh object for every one of the million characters in a file — it shares one glyph object per distinct character. Java caches small Integer boxes, and most languages intern strings so equal literals point at one shared object. Flyweight is everywhere; it just usually hides inside the platform.

Mechanics

How it works

Two kinds of state: intrinsic vs extrinsic

The whole pattern is one act of sorting. Take everything an object stores and split it into two piles:

  • Intrinsic state — the part that is the same across many objects and doesn't change: the sprite, the color, the texture, the name Oak. This lives inside the flyweight (TreeType) and is shared by everyone. Intrinsic = belongs to the type.
  • Extrinsic state — the part that is unique to each object: this tree's x and y. This is kept outside the flyweight — stored in the small per-tree object, or passed in as method arguments (type.draw(x, y)). Extrinsic = belongs to the instance.

A quick self-test for any field: if I made 10,000 of these, how many distinct values would this field have? Three values (species sprite) → intrinsic, share it. Ten thousand values (position) → extrinsic, keep it per object. Get this split right and the rest of the pattern writes itself.

The factory cache: build each flyweight once

Nobody creates a TreeType directly. Everyone asks a `TreeFactory`, which keeps a cache (a map from a key like "Oak" to the flyweight). The first request for Oak builds the heavy TreeType and stores it; every later request returns the already-built, shared one. Plant 10,000 trees across 3 species and the factory constructs exactly 3 heavy objects — the other 9,997 requests are cache hits. It's the same 'check the cache, build once, reuse forever' move a Singleton's getInstance() makes, except the factory manages a family of instances, one per key.

typescript
class TreeFactory {
  private static cache = new Map<string, TreeType>();

  static get(name: string, color: string, sprite: string): TreeType {
    let type = TreeFactory.cache.get(name);
    if (!type) {                                  // first time this species?
      type = new TreeType(name, color, sprite);   // build the heavy 2 KB once
      TreeFactory.cache.set(name, type);
    }
    return type;                                  // everyone shares this one
  }
}

Why flyweights MUST be immutable

One TreeType is referenced by thousands of trees at once. If any code could write oakType.color = "purple", every oak in the forest would silently turn purple — a change made 'to one tree' would hit thousands of unrelated ones. That's why flyweights are immutable: all fields set in the constructor, no setters, read-only forever. Immutability is what makes sharing safe — this is the same reasoning behind value objects in [[immutability-and-value-objects]]. If a value needs to differ per tree or change over time, it isn't intrinsic — move it out to extrinsic state.

Flyweight is not Object Pool

Both reuse objects, but differently. A pool ([[object-pool]]) hands out objects over time: one user at a time borrows an object, mutates it, and returns it. Flyweight shares objects simultaneously: thousands of owners hold the same instance at the same moment, which is exactly why it must be immutable. Pool = reuse across time, mutable. Flyweight = share right now, frozen.

The memory math

  • Without Flyweight: 10,000 trees × 2 KB each ≈ 20 MB, and it grows linearly — every new tree costs another full 2 KB.
  • With Flyweight: 3 shared TreeTypes × 2 KB + 10,000 trees × ~40 B (an x, a y, a reference) ≈ 6 KB + 400 KB ≈ 0.4 MB — roughly 50× less.
  • The win scales with count × duplication: the heavy cost is paid per species (3, fixed), while the per-tree cost is tiny. Doubling the forest adds ~0.4 MB, not ~20 MB.

Where you meet this in the real world

  • Text editors — one shared glyph object per distinct character; each character position in the document stores only which glyph plus where it sits. A million-character file needs ~100 glyph objects, not a million.
  • String interning"hello" written in two places points at one shared string object; the runtime keeps an intern table (a flyweight factory for strings).
  • `Integer.valueOf()` in Java — values from −128 to 127 come from a shared cache, so boxing small numbers doesn't allocate.
  • Browsers — thousands of DOM nodes with identical styling share one computed-style object instead of each storing its own copy.
  • Games — particles, bullets, tiles and props: thousands of instances, a handful of shared sprite/mesh 'types'. Flyweight is the standard trick.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

A game forest you fill with tree emojis. Flip between two worlds. In ✕ WITHOUT Flyweight, every click of + 50 trees or + 200 trees stamps trees that each carry their own 2 KB copy of sprite data — watch the 🧠 memory chip and the memory bar climb fast and turn red, and heavy objects equal the tree count. In ✓ WITH Flyweight, the exact same forest (same positions, same species — it's seeded) is recounted: a small TreeType cache strip shows just 3 shared cards (🌳 Oak, 🌲 Pine, 🌴 Palm), each created once and then reused ×N, while every tree stores only x, y plus a reference. The bar barely moves and stays green. Same trees, ~50× less memory — that contrast is the pattern.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Watch memory explode without sharing

Open the prototype with the toggle on ✕ WITHOUT Flyweight. Click + 50 trees a few times, then + 200 trees. Watch the three chips: 🌳 trees climbs, heavy objects climbs in lockstep (every tree owns its own 2 KB copy), and 🧠 memory races up — the memory bar stretches and turns red. That linear climb is 'count × 2 KB' with nothing shared.

try 02

Flip to WITH and compare the same forest

Switch the toggle to ✓ WITH Flyweight. The forest is identical — same positions, same species — but the accounting changes: heavy objects drops to at most 3, and the memory bar collapses to a green sliver. The TreeType cache strip appears with 3 cards (🌳 Oak, 🌲 Pine, 🌴 Palm), each marked 2 KB · shared with a reused ×N badge. Those 3 cards are the only heavy data in the whole scene.

try 03

Prove new trees are nearly free

Still in ✓ WITH Flyweight, click + 200 trees several times. The reused ×N badges tick up, heavy objects stays pinned at 3, and 🧠 memory barely moves — each new tree adds only ~40 bytes of x, y + reference. Then hit ↺ Reset, plant in WITH first and watch each card flash 'created once' exactly one time: the factory builds each species once, then every later tree is a cache hit.

In practice

When to use it — and what trips people up

Reach for Flyweight when all three line up

  • Huge object counts — thousands to millions of instances alive at the same time: game entities, characters in a document, map markers, table cells.
  • Heavy, repeated state — a big chunk of each object (sprite, glyph, style, config blob) is duplicated across many of them, with far fewer distinct values than instances.
  • The shared part can be immutable — you can freeze the repeated chunk and pass the changing bits (position, selection, velocity) in from outside.
  • Memory is actually the constraint — profiling (or arithmetic like 10,000 × 2 KB) shows the duplication is what's hurting you.

Skip it when

  • Object counts are modest — a few hundred objects duplicating 2 KB is ~half a megabyte; the pattern's complexity isn't buying anything.
  • State is mostly unique — if almost every field differs per instance, there's nothing worth sharing; a flyweight of one field saves nothing.
  • The 'shared' part must mutate — if per-object changes to that data are needed, sharing it is a bug factory, not an optimization.
  • You haven't measured — Flyweight splits one simple class into three parts (flyweight, factory, context). Don't pay that readability cost before a profile or back-of-envelope math shows a real memory problem.

It's an optimization pattern — treat it like one

Unlike most patterns, Flyweight changes cost, not capability. The forest renders identically either way. So apply it the way you'd apply any optimization: measure first, confirm the duplication is the problem, then restructure — and keep the factory as the single door so callers never notice the sharing.

What it gives you

  • Massive memory savings when duplication is real — the heavy cost is paid per distinct value (3 species), not per instance (10,000 trees): ~20 MB becomes ~0.4 MB.
  • Scales gracefully — once the flyweights exist, each extra object costs only its tiny extrinsic record, so doubling the count barely moves memory.
  • Fewer allocations and better cache behavior — 3 heavy constructions instead of 10,000, and thousands of objects reading the same shared data keeps it hot.
  • The factory centralizes creation — one place to build, count and inspect shared state, invisible to callers who just ask for 'an Oak'.

Common mistakes

  • Trades CPU for RAM — extrinsic state must be passed in or looked up on every operation, and the factory does a cache lookup per request; that's extra work per call.
  • More moving parts — one plain class becomes flyweight + factory + context, and the intrinsic/extrinsic split makes the code harder to read for newcomers.
  • The immutability requirement is rigid — the moment someone needs to tweak shared state 'for just one tree', the design fights back (or worse, they mutate it and corrupt thousands of objects).
  • Cached flyweights live forever by default — the factory's map holds strong references, so rarely-used flyweights are never freed unless you add eviction or weak references.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// FLYWEIGHT — heavy intrinsic state, shared and IMMUTABLE.
class TreeType {
  constructor(
    readonly name: string,     // "Oak"
    readonly color: string,    // shared by every oak
    readonly sprite: string,   // pretend: ~2 KB of texture data
  ) {}
  // Extrinsic state (x, y) is passed IN — never stored here.
  draw(x: number, y: number): void {
    console.log(`${this.name} at (${x}, ${y})`);
  }
}

// FACTORY — cache: build each species once, then reuse.
class TreeFactory {
  private static cache = new Map<string, TreeType>();
  static get(name: string, color: string, sprite: string): TreeType {
    let t = TreeFactory.cache.get(name);
    if (!t) {                                   // miss → build the 2 KB once
      t = new TreeType(name, color, sprite);
      TreeFactory.cache.set(name, t);
    }
    return t;                                   // hit → the shared instance
  }
  static created(): number { return TreeFactory.cache.size; }
}

// CONTEXT — tiny per-tree record: extrinsic state + a reference.
class Tree {
  constructor(private x: number, private y: number,
              private type: TreeType) {}        // reference, NOT a copy
  draw(): void { this.type.draw(this.x, this.y); }
}

// 10,000 trees, 3 species → only 3 heavy objects ever exist.
const species = [["Oak", "green"], ["Pine", "teal"], ["Palm", "sand"]];
const forest: Tree[] = [];
for (let i = 0; i < 10_000; i++) {
  const [name, color] = species[i % 3];
  const type = TreeFactory.get(name, color, name + ".png");
  forest.push(new Tree(Math.random() * 800, Math.random() * 600, type));
}
console.log(TreeFactory.created());  // 3 — not 10,000

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What problem does the Flyweight pattern solve?

question 02 / 05

In the game forest, which is intrinsic state and which is extrinsic?

question 03 / 05

Why must flyweight objects be immutable?

question 04 / 05

You plant 10,000 trees across 3 species through a TreeFactory. How many heavy TreeType objects exist, and roughly what memory do you save versus no sharing (2 KB heavy data, ~40 B per tree record)?

question 05 / 05

How is Flyweight different from Object Pool, since both 'reuse objects'?

0/5 answered