Flyweight Pattern: Sharing State to Cut Memory Usage
How to minimize memory usage by sharing intrinsic state across millions of similar objects, using a flyweight factory, and when the added complexity is actually worth it.
Flyweight Pattern
Intent: use sharing to support large numbers of fine-grained objects efficiently. Flyweight minimizes memory usage by splitting an object's state into two categories — intrinsic state (shared, immutable, context-independent) and extrinsic state (unique per usage, supplied by the client) — and sharing one instance per distinct intrinsic-state combination instead of allocating a new object per logical entity.
This is the pattern for the specific situation where you have a huge number of logically distinct objects that are, underneath, mostly identical. The canonical textbook example is a text editor representing every character on the page as an object — millions of them — where most of that per-character state (font, size, color) is identical across large runs of text.
Flyweight is named after boxing's lightest weight class — the whole point is making each shared object as small and cheap as possible so you can have vastly more of them than memory would otherwise allow. The "weight" being minimized is per-object memory footprint, achieved by moving anything shareable out of the per-instance object and into a pool.
1. Intrinsic vs. extrinsic state
Before the code, the vocabulary, because it's what interviewers actually probe:
| Intrinsic state | Extrinsic state | |
|---|---|---|
| Definition | State that's shareable — identical across many logical objects, independent of where/how the object is used | State that's unique per usage — depends on the specific context the object appears in |
| Where it lives | Stored once, inside the shared flyweight object | Stored outside the flyweight, held by the client, and passed in on each call |
| Mutability | Must be immutable — shared state that could be mutated by one user would corrupt it for every other user of the same flyweight | Can vary freely — it's owned by the caller, not the shared object |
| Character example | Glyph shape, font family, font size (a run of "Times New Roman, 12pt, A" characters all share one flyweight) | Position on the page (x, y coordinates) — every 'A' is at a different spot |
Notice: three flyweight objects (FA, FB, FC) serve four occurrences, and in a real document, millions of occurrences. Two different 'A' occurrences at different positions share the exact same FA flyweight — the position lives outside it.
2. The problem, in code
VIOLATION: one full object per character occurrence
// VIOLATION: every character on the page — position AND styling AND glyph data — bundled into one object
class Character {
private final char symbol;
private final String fontFamily;
private final int fontSize;
private final Color color;
private final byte[] glyphBitmap; // the actual rendered glyph shape — can be tens of KB
private int x, y; // position on the page — genuinely unique per occurrence
Character(char symbol, String fontFamily, int fontSize, Color color, byte[] glyphBitmap, int x, int y) {
this.symbol = symbol;
this.fontFamily = fontFamily;
this.fontSize = fontSize;
this.color = color;
this.glyphBitmap = glyphBitmap; // duplicated in EVERY character object, even identical ones
this.x = x;
this.y = y;
}
}
class Document {
private final List<Character> characters = new ArrayList<>();
void addCharacter(char symbol, String font, int size, Color color, byte[] glyph, int x, int y) {
characters.add(new Character(symbol, font, size, color, glyph, x, y));
}
}
// A 500,000-character document in one font: 500,000 copies of the SAME glyph
// bitmap data, differing only in symbol, x, and y. Memory usage scales with
// character COUNT, when it should scale with DISTINCT (symbol, font, size) COMBINATIONS.For a document using, say, 90 distinct (character, font, size, color) combinations across 500,000 characters, this representation allocates 500,000 copies of glyph data that only needed to exist 90 times.
3. The fix: separate the shareable part into a factory-managed pool
// Flyweight: ONLY intrinsic (shareable, immutable) state
final class CharacterGlyph {
private final char symbol;
private final String fontFamily;
private final int fontSize;
private final byte[] glyphBitmap;
CharacterGlyph(char symbol, String fontFamily, int fontSize, byte[] glyphBitmap) {
this.symbol = symbol;
this.fontFamily = fontFamily;
this.fontSize = fontSize;
this.glyphBitmap = glyphBitmap; // exists ONCE per distinct combination
}
// Extrinsic state (position, color) is passed in per call, never stored here
void render(int x, int y, Color color) {
// draw glyphBitmap at (x, y) tinted with color
}
}
// Flyweight factory: the "intern pool" — guarantees one instance per distinct key
class GlyphFactory {
private final Map<String, CharacterGlyph> pool = new HashMap<>();
CharacterGlyph getGlyph(char symbol, String fontFamily, int fontSize) {
String key = symbol + "|" + fontFamily + "|" + fontSize;
return pool.computeIfAbsent(key, k ->
new CharacterGlyph(symbol, fontFamily, fontSize, loadGlyphBitmap(symbol, fontFamily, fontSize)));
}
private byte[] loadGlyphBitmap(char symbol, String font, int size) {
// expensive: rasterize or load from a font file — now only happens
// once per distinct (symbol, font, size), not once per occurrence
return new byte[0];
}
int poolSize() { return pool.size(); }
}
// Client holds extrinsic state (position, color) and the SHARED flyweight reference
class CharacterOccurrence {
private final CharacterGlyph glyph; // shared — many occurrences point to the same instance
private final int x, y; // extrinsic — unique per occurrence
private final Color color; // extrinsic — unique per occurrence
CharacterOccurrence(CharacterGlyph glyph, int x, int y, Color color) {
this.glyph = glyph;
this.x = x; this.y = y; this.color = color;
}
void render() {
glyph.render(x, y, color); // extrinsic state supplied at call time
}
}
class Document {
private final GlyphFactory glyphFactory = new GlyphFactory();
private final List<CharacterOccurrence> occurrences = new ArrayList<>();
void addCharacter(char symbol, String font, int size, Color color, int x, int y) {
CharacterGlyph glyph = glyphFactory.getGlyph(symbol, font, size); // reuses existing instance if present
occurrences.add(new CharacterOccurrence(glyph, x, y, color));
}
}Now a 500,000-character document with 90 distinct (symbol, font, size) combinations allocates exactly 90 CharacterGlyph instances (holding the expensive glyph bitmaps) and 500,000 lightweight CharacterOccurrence instances holding only a shared reference plus a few primitives.
The factory is not optional — it's the mechanism that actually enforces sharing. Without GlyphFactory.getGlyph() funneling every request through the pool's computeIfAbsent, callers could still accidentally new CharacterGlyph(...) directly and defeat the whole pattern. In real code, make the flyweight's constructor package-private or force construction exclusively through the factory.
4. String interning: Flyweight already built into the JDK
Java's string pool is a production Flyweight you've been using without necessarily naming it. String literals with identical content are, by default, the same object:
String a = "hello";
String b = "hello";
System.out.println(a == b); // true — both reference the same pooled String instance
String c = new String("hello"); // explicitly bypasses the pool — new heap object
String d = c.intern(); // forces it into (or retrieves it from) the pool
System.out.println(a == d); // true — d now references the shared flyweight
System.out.println(a == c); // false — c is a separate, un-pooled instanceHere, the string's character content is the intrinsic state (immutable, shareable — String in Java is immutable specifically so this sharing is safe), and there's no extrinsic state at all in this simple case, because the "occurrence" (each variable holding a reference) needs nothing beyond the shared value itself. This is the degenerate but extremely common case of Flyweight: sometimes there's no meaningful extrinsic state to separate out, and the pattern collapses into "just share the immutable value."
Integer.valueOf(int) does the same thing for the range -128 to 127 (the "Integer cache") — Integer.valueOf(100) == Integer.valueOf(100) is true, but Integer.valueOf(200) == Integer.valueOf(200) is false, because only the small, statistically common range is pooled.
5. When Flyweight is worthwhile
| Use Flyweight when | Skip it when |
|---|---|
| You have a very large number of objects (thousands to millions) | Object count is small (dozens to low thousands) — the memory saved doesn't justify the complexity |
| A significant portion of each object's state is identical across many instances | Most state is genuinely unique per instance — there's little to share |
| The shareable portion can be made truly immutable | The "shareable" state actually needs to vary per instance sometimes — forcing it to be intrinsic would introduce bugs |
| You've measured (not guessed) that per-object memory overhead is an actual problem | You haven't profiled memory usage — this is a classic premature optimization trap |
The trade-off is real: memory savings vs. code complexity. Flyweight adds a factory, a pool, and a permanent split between intrinsic and extrinsic state that every future change to the object's fields has to respect — get the split wrong (accidentally putting mutable, context-specific state into the "shared" flyweight) and you introduce a correctness bug where one caller's changes silently leak into another caller's rendering. Reach for Flyweight only once profiling shows object count/memory is the actual bottleneck — not because "sharing sounds efficient."
6. Flyweight vs. Singleton
Both patterns are frequently confused because both involve a factory-like mechanism controlling instance creation, but the cardinality is the entire distinction:
| Flyweight | Singleton | |
|---|---|---|
| How many shared instances exist | Many — one per distinct intrinsic-state combination | Exactly one, globally, for the entire application |
| Purpose | Reduce memory by sharing state across a large number of logical objects | Ensure a single, globally consistent instance of something (a config store, a connection pool) |
| Key that determines identity | The intrinsic state's value (two flyweights with the same intrinsic state are meant to be the same instance) | None needed — there's only ever one, no keying by value |
| Client-supplied state | Yes — extrinsic state is essential to the pattern | Not applicable — a singleton typically doesn't take per-call context to determine "which instance" |
A useful phrasing: Singleton answers "how many instances of this class should ever exist — one, period." Flyweight answers "how many instances of this class should exist — as few as the distinct values actually require, could be dozens, could be thousands, but never one more than that." A GlyphFactory's pool is really "many small singletons, one per key" rather than "the one singleton."
7. Real-world flyweights
String.intern()and Java's compile-time string literal pool, covered above.Integer.valueOf,Boolean.valueOf,Byte.valueOfand the other boxed-primitive caches for commonly used small values.- Font glyph caches in real rendering engines (browsers, PDF renderers, game engines) — exactly the character/glyph example above, at production scale.
- Tile-based game maps — a
Tiletype (grass, water, wall) holding shared sprite/texture/collision data is a flyweight; the map stores only(tileType, x, y)per cell rather than a full tile object per cell. - Connection/thread pools are adjacent in spirit (reuse expensive objects instead of allocating per-use) but are a different pattern — Object Pool — because pooled connections are mutually exclusive (checked out one caller at a time), whereas flyweights are concurrently shared by design since their shared state is immutable.
Interview Questions
- Define intrinsic and extrinsic state in your own words, using the character/glyph example.
- Why must intrinsic state be immutable for Flyweight to be safe? What breaks if it isn't?
- Walk through the memory math: for a 500,000-character document with 90 distinct (symbol, font, size) combinations, how many
CharacterGlyphobjects exist with and without Flyweight? - Why is the flyweight factory not optional — what does it actually enforce that a plain constructor call wouldn't?
- Explain how Java's string interning is an example of Flyweight, including what plays the role of intrinsic state and why there's effectively no extrinsic state in that example.
- Compare Flyweight and Singleton. Both involve a controlled creation mechanism — what's the actual cardinality difference?
- What's the risk of applying Flyweight before you've actually measured a memory problem?
- How is Flyweight different from a generic object pool (like a JDBC connection pool)? Why can flyweights be safely shared concurrently while pooled connections can't?