Flyweight
Some apps need to create an enormous number of very similar objects — every tree in a forest, every character on a page. The Flyweight pattern answers a memory question: how do you support hundreds of thousands of objects without storing hundreds of thousands of full copies of their data?
⚠️ The Problem
Render a forest of 500,000 trees, and a naive Tree object per tree stores
everything: an x/y position, but also a full 3D mesh and a texture bitmap — identical for
every pine tree in the forest. If the mesh and texture data are megabytes and get duplicated
into every one of 500,000 objects, memory use explodes for data that's actually the same
across nearly all of them. The position genuinely differs per tree; the mesh and texture do
not — but a naive object model stores both as if they were equally unique.
✅ The Solution
Split each object's state into two kinds. Intrinsic state is shared and
immutable — the mesh and texture for "pine tree" — and gets stored exactly once in a shared
TreeType flyweight object, reused by every tree of that species.
Extrinsic state is unique per instance — the x/y position — and is passed in
by the caller at the moment it's needed (e.g. to a draw(x, y) call) rather than
stored inside the flyweight itself. A factory keeps a lookup of flyweights already created,
so requesting "pine tree" a second time returns the same shared object instead of building a
new one.
Example 1 — a forest of trees
// The Flyweight: shared, immutable intrinsic state (mesh + texture), created once per species.
class TreeType {
constructor(name, mesh, texture) {
this.name = name;
this.mesh = mesh; // large shared data
this.texture = texture; // large shared data
}
draw(x, y) {
console.log(`Drawing ${this.name} at (${x}, ${y}) using shared mesh/texture`);
}
}
// Factory: ensures identical intrinsic state is only ever created once.
class TreeTypeFactory {
static #types = new Map();
static get(name, mesh, texture) {
const key = name;
if (!TreeTypeFactory.#types.has(key)) {
console.log(`Creating new TreeType flyweight for "${name}"`);
TreeTypeFactory.#types.set(key, new TreeType(name, mesh, texture));
}
return TreeTypeFactory.#types.get(key);
}
}
// The per-instance object: stores only extrinsic state (position) + a reference to the flyweight.
class Tree {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type; // shared TreeType, not a private copy
}
draw() {
this.type.draw(this.x, this.y);
}
}
// Planting 500,000 trees of 3 species creates only 3 TreeType objects, not 500,000.
const forest = [];
const species = [
["Pine", "pine-mesh", "pine-texture"],
["Oak", "oak-mesh", "oak-texture"],
["Birch", "birch-mesh", "birch-texture"],
];
for (let i = 0; i < 500000; i++) {
const [name, mesh, texture] = species[i % 3];
const type = TreeTypeFactory.get(name, mesh, texture); // reused after the first 3 calls
forest.push(new Tree(Math.random() * 1000, Math.random() * 1000, type));
}
forest[0].draw();
forest[1].draw();
Only three TreeType flyweights ever get created, no matter how many
Tree instances exist — each Tree is cheap, storing just a position and
a reference to shared mesh/texture data.
Example 2 — character formatting in a text editor
import java.util.HashMap;
import java.util.Map;
// The Flyweight: shared, immutable intrinsic state — a character rendered in a given font.
class Glyph {
private final char character;
private final String fontFamily;
private final int fontSize;
Glyph(char character, String fontFamily, int fontSize) {
this.character = character;
this.fontFamily = fontFamily;
this.fontSize = fontSize;
}
void render(int x, int y) {
System.out.printf("Rendering '%c' [%s %dpt] at (%d, %d)%n",
character, fontFamily, fontSize, x, y);
}
}
// Factory: caches one Glyph per (character, font, size) combination.
class GlyphFactory {
private static final Map<String, Glyph> cache = new HashMap<>();
static Glyph get(char character, String fontFamily, int fontSize) {
String key = character + "|" + fontFamily + "|" + fontSize;
return cache.computeIfAbsent(key, k -> {
System.out.println("Creating new Glyph flyweight for key: " + k);
return new Glyph(character, fontFamily, fontSize);
});
}
}
// Extrinsic state (position in the document) is passed in at render time, not stored in the glyph.
class CharacterPosition {
final Glyph glyph;
final int x, y;
CharacterPosition(Glyph glyph, int x, int y) {
this.glyph = glyph;
this.x = x;
this.y = y;
}
void render() {
glyph.render(x, y);
}
}
// A 50,000-word document reuses a small number of Glyph flyweights per character/font/size.
Glyph h = GlyphFactory.get('H', "Inter", 12);
Glyph e = GlyphFactory.get('e', "Inter", 12);
CharacterPosition p1 = new CharacterPosition(h, 0, 0);
CharacterPosition p2 = new CharacterPosition(e, 8, 0);
p1.render();
p2.render();
Every occurrence of the letter "e" in 12pt Inter across the whole document shares one
Glyph flyweight — only its position differs, and that position is stored outside
the flyweight, in CharacterPosition.
👍 When To Use It
- You need a very large number of objects, and profiling (or plain arithmetic) shows memory use from duplicated shared data is a real problem, not a theoretical one.
- Most of each object's state can be cleanly split into "shared and immutable" versus "unique per instance" — if that split is muddy, Flyweight adds complexity for no payoff.
- The shared (intrinsic) state genuinely doesn't change once created — flyweights are meant to be safely shared by many owners simultaneously.
👎 When NOT To Use It
- When object counts are modest — Flyweight's factory/lookup machinery is extra complexity that only pays off at real scale (thousands to millions of instances).
- When most of an object's state is actually unique per instance — there's little to share, so splitting intrinsic/extrinsic state barely reduces memory and just adds indirection.
- When the "shared" state might need to change independently per instance later — that pressure tends to break the intrinsic/extrinsic split and force a redesign.
❓ FAQ
Isn't this just caching?
It rhymes with caching — a factory returns an existing object instead of creating a new one — but Flyweight is specifically about splitting an object's state into shared/immutable (intrinsic) and per-instance (extrinsic) parts, and structuring the whole design around that split. Generic caching doesn't require that split; it just avoids recomputation.
Who is responsible for storing the extrinsic state?
The client/caller, not the flyweight. In the examples above, Tree stores
the position and passes it into TreeType.draw(x, y); the flyweight itself
never holds it. This is what keeps a single flyweight instance safely shareable across
every owner that needs it.
Can flyweights be mutable?
In principle no — mutable shared state would mean changing it for one "owner" changes it for every other owner sharing that flyweight, which is almost never what you want. Flyweights are meant to be immutable once constructed; anything that needs to vary per instance belongs in the extrinsic state instead.
🙋 There Are No Dumb Questions
Q: If I only have a few hundred objects, is Flyweight ever still worth it?
A: Rarely. Flyweight trades code complexity (a factory, a lookup, splitting state into two
categories) for memory savings — and at a few hundred objects, that memory savings is
usually negligible while the added indirection is not. It earns its keep specifically at the
scale where duplicated shared data would otherwise be a measurable problem.
✅ Quick Check
In the Flyweight pattern, where does the shared, immutable data live?
Next up: what if you need to control access to an object — delaying its creation until it's really needed, or checking permissions before letting a call through? That's the Proxy pattern.