Flyweight Pattern
Share common state across thousands of objects to slash memory usage. An advanced structural pattern — the engine behind Java’s String pool, Integer cache, and every text editor that renders millions of characters.
Why Flyweight Exists
You are building a text editor. Every character on-screen is an object: it carries the Unicode code point, the font family, the font size, the colour, bold/italic flags, and its (x, y) position. A modest document — 100,000 characters — creates 100,000 objects. Each object stores the same font/size/colour metadata that thousands of neighbours share. At ~200 bytes per object, that’s 20 MB for a plain text file. Open five documents and you’ve consumed 100 MB of heap — for text.
What goes wrong:
- Massive memory waste — 10,000 characters with font “Arial”, size 12, colour black each store their own copy of “Arial”, 12,
Color.BLACK. The same data is duplicated 10,000 times. - GC pressure — 100,000 small objects create enormous garbage-collection overhead. Young-generation collections become frequent; pause times increase.
- Doesn’t scale — a game rendering 1,000,000 trees in a forest, each with the same mesh/texture but different (x, y, z) position, would need 1,000,000 full tree objects. Impossible on limited hardware.
- Cache misses — bloated objects don’t fit in CPU cache lines. Iterating 100,000 characters becomes slower than it needs to be because each object drags in unused shared data.
This is the problem Flyweight solves. Split each object into two parts: intrinsic state (font, size, colour, bold — shared by thousands of characters) and extrinsic state (row, col — unique to each character). Store the intrinsic state in a single shared flyweight object; pass the extrinsic state in from outside. 10,000 characters that use “Arial 12pt Black” all point to one flyweight instead of storing 10,000 copies. Memory drops from 20 MB to kilobytes.
What Is Flyweight?
Flyweight is a structural pattern that lets you fit more objects into memory by sharing common state between multiple objects instead of keeping all data in each object. The key insight: split an object’s state into intrinsic (shared, immutable, context-independent) and extrinsic (unique, context-dependent, supplied by the caller). The intrinsic state lives in a shared flyweight; the extrinsic state is passed in at operation time.
— Gamma, Helm, Johnson, Vlissides (1994)
The two kinds of state:
| Intrinsic State | Extrinsic State | |
|---|---|---|
| Definition | Data that is shared across many objects. Independent of context. Immutable. | Data that is unique to each usage. Depends on context. Varies per call. |
| Where it lives | Inside the flyweight object (stored once, shared by many) | Outside the flyweight — passed in as a method parameter by the client |
| Text editor example | Font family, font size, colour, bold/italic | Row and column position on screen |
| Game forest example | Tree mesh, texture, colour palette | x, y, z world position for each tree instance |
| Mutability | Must be immutable — shared objects cannot be modified by one user without affecting all others | Can change freely — each caller owns their own extrinsic state |
new Flyweight() directly.render(row, col)). No duplication.- All three involve reusing objects, but for different reasons.
- Singleton ensures exactly one instance exists globally (identity matters).
- Cache stores recently computed results for performance (temporal reuse).
- Flyweight shares immutable state across many fine-grained objects to reduce memory (spatial reuse — many objects share the same data).
- A flyweight factory uses a cache internally, but the pattern’s purpose is memory optimisation through structural sharing, not speed.
Participants & Structure
| Participant | Role | In the Text Editor |
|---|---|---|
| Flyweight | An interface (or class) declaring an operation that accepts extrinsic state as a parameter. The flyweight stores only intrinsic state — shared, immutable data. Must be safe to share across all users. | CharStyle — stores font, size, colour, bold. Its render(char c, int row, int col) method accepts position (extrinsic) at call time. |
| ConcreteFlyweight | Implements the Flyweight interface. Stores intrinsic state. Is immutable. Many clients hold references to the same ConcreteFlyweight instance. | A specific CharStyle("Arial", 12, BLACK, false) object — shared by every character in the document that uses that style. |
| FlyweightFactory | Creates and manages flyweight objects. Maintains a pool (usually a Map). When the client requests a flyweight, the factory returns an existing one if possible, or creates and caches a new one. This guarantees sharing. | CharStyleFactory — keeps a Map<StyleKey, CharStyle>. Calling getStyle("Arial", 12, BLACK, false) returns the cached instance or creates it. |
| Client | Maintains references to flyweights and computes or stores the extrinsic state. The client passes extrinsic state to the flyweight when calling its operation. The client never creates flyweights directly — it always goes through the factory. | The TextEditor stores a list of (char, row, col, CharStyle) tuples. When rendering, it calls style.render(c, row, col) for each character. |
- Without the
CharStyleFactory, clients would create flyweights directly withnew, and sharing would be accidental. - The factory is the single point of creation that enforces sharing.
- It’s typically implemented with a
HashMapkeyed by the intrinsic state values. - If the key exists, return the cached flyweight; otherwise create, cache, and return it.
- This is why Flyweight almost always appears alongside a factory.
The Pattern In Motion
Scenario: A text editor renders a document. Characters share styling flyweights; position info is supplied at render time. Walk through the lifecycle of the flyweight cache.
factory.getStyle("Arial", 12, BLACK, false). Factory checks its cache — empty. Creates a new CharStyle, caches it, returns it. Cache size: 1.factory.getStyle("Arial", 12, BLACK, false). Factory finds the cached flyweight โ returns it. No new objects created. 10,000 characters share one flyweight instance. Cache size: still 1.factory.getStyle("Arial", 18, BLACK, true) — new key, not in cache. Factory creates a second flyweight, caches it. 5 heading characters share this new flyweight. Cache size: 2.style.render(c, row, col)render() method. The flyweight combines its intrinsic state (font/size/colour) with the supplied position โ draws the character on screen.- Java records are immutable by default, give you
equals()/hashCode()for free (based on all fields), and have a compact declaration. - This makes them ideal flyweights: immutability guarantees safe sharing, and the auto-generated
hashCode()makes them perfectHashMapkeys for the factory cache. - Using records for flyweights is a modern Java best practice.
You Already Use This
Flyweight is baked into Java’s core — every time you use a String literal, box a small integer, or compare Boolean values, the JVM is sharing flyweight objects behind your back.
Integer.valueOf(int) — the textbook JVM flyweight. For values −128 to 127, valueOf() returns a cached instance from an internal Integer[] array. Integer.valueOf(42) == Integer.valueOf(42) is true (same object). Outside that range, a new Integer is allocated every time. This is why == on boxed integers is unreliable — it only works for flyweight-cached values. Byte, Short, Long, and Character (0–127) use the same strategy. String interning — the String pool. All string literals ("hello") are automatically interned: the JVM keeps a pool and reuses the same String object for identical literals. "hello" == "hello" is true because both point to the same flyweight. Calling str.intern() manually adds a runtime string to the pool. This saves enormous memory in applications with many repeated string values (config keys, JSON field names, enum-like strings). Boolean.valueOf(boolean) — returns Boolean.TRUE or Boolean.FALSE, the same two cached instances every time. There are only two possible values, so they are always shared. Boolean.valueOf(true) == Boolean.TRUE is always true. This is the simplest possible flyweight: the factory (the valueOf method) always returns a cached object. java.util.regex.Pattern — compiling a regex is expensive (parsing, building an NFA/DFA). Frameworks like Spring and many libraries cache compiled Pattern objects and reuse them. The compiled pattern (intrinsic state โ the regex structure) is shared; the input string to match against (extrinsic state) is passed in at matcher(input) call time. While not a pure GoF Flyweight, the structure is identical: immutable shared object + extrinsic state supplied per use. - When you write
Integer x = 42;, the compiler auto-insertsInteger.valueOf(42), which uses the flyweight cache. - This is why two autoboxed
intvalues in the −128..127 range compare as==. - Understanding Flyweight explains one of Java’s most common interview gotchas: “Why does
new Integer(5) == new Integer(5)returnfalsebutInteger.valueOf(5) == Integer.valueOf(5)returntrue?”
Build It Once
Domain: Game Forest Renderer. A TreeType flyweight stores shared mesh/texture data; a Tree holds only its (x, y) position and a reference to the shared type. A TreeTypeFactory ensures each unique tree type is created only once.
- In the Java implementation,
TreeTypeis arecord(immutable, autoequals/hashCode) and the factory usesMap.computeIfAbsent()for atomic get-or-create. - This two-line factory is the idiomatic modern Java way to implement Flyweight.
- For thread-safety, swap
HashMapforConcurrentHashMap—computeIfAbsentis already atomic onConcurrentHashMap.
Common Mistakes
records, final fields, no setters. If you need different state, request a different flyweight from the factory.
- Flyweight only saves memory when many objects share the same intrinsic state.
- If every object has unique intrinsic state, the factory cache grows to N entries (one per object) and you’ve added complexity for zero benefit.
- Rule of thumb: profile first.
- If your cache hit rate is below 50%, Flyweight is the wrong pattern — you’re paying the factory overhead without meaningful sharing.
- If intrinsic-state combinations are unbounded (e.g. arbitrary RGB colours), the factory cache grows indefinitely — a memory leak disguised as an optimisation.
- Fix: use a bounded cache (
LinkedHashMapwithremoveEldestEntry), aWeakHashMap, or Caffeine/Guava cache with eviction policies. Monitor cache size in production.
When To Use Flyweight
- Your application creates a very large number of similar objects (tens of thousands+) and memory is a bottleneck
- Most of each object’s state can be classified as intrinsic (shared, immutable) — e.g. font/colour/texture data duplicated across thousands of instances
- The extrinsic state is small and cheap to pass (a few ints, a reference) compared to the intrinsic state being shared
- Object identity doesn’t matter — clients don’t need
==to distinguish flyweights from each other; they care about value equality - You can factor out the shared state into a finite, manageable number of flyweight instances (the cache remains small)
- You only have a small number of objects — the factory overhead isn’t worth it for 50 instances
- Every object has unique intrinsic state — cache hit rate is near zero, no sharing occurs
- The extrinsic state is large or expensive to pass around — the API becomes awkward and the savings vanish
- Objects need to be mutable — shared flyweights must be immutable; if clients need to modify state, Flyweight is the wrong pattern
- You haven’t profiled yet — don’t optimise for memory until you’ve measured that memory is the actual problem
| Pattern | Purpose | Sharing? | When to pick it |
|---|---|---|---|
| Flyweight ← this | Share intrinsic state across many objects | Yes (many-to-one) | Thousands of similar objects, memory is the bottleneck |
| Singleton | Exactly one instance globally | One instance total | System-wide unique resource (config, logger) |
| Prototype | Clone objects instead of constructing from scratch | No (each clone is independent) | Expensive construction, need independent copies |
| Object Pool | Reuse expensive-to-create objects (connections, threads) | Borrowed/returned, not shared simultaneously | Creation cost is high; objects are used one-at-a-time |
| Cache | Store computed results for reuse | Temporal reuse (same result, different times) | Computation is expensive, results are deterministic |
Problems To Solve
Flyweight problems test whether you can identify intrinsic vs. extrinsic state, design an immutable flyweight with a factory cache, pass extrinsic state at operation time, and measure the memory savings.
| Difficulty | Problem | Key Insight |
|---|---|---|
| Easy | Colour Palette Flyweight A drawing application lets users place dots on a canvas. Each dot has a colour (RGB string like "#FF5733") and a position (x, y). Users typically use 5–10 colours but place 100,000+ dots. Build a ColorFlyweight that stores the parsed RGB value, a ColorFactory that caches colours, and a Dot that holds only (x, y) + a reference to the shared colour. Print the cache size vs. total dots to show the savings. | The simplest Flyweight exercise: one intrinsic field (colour), two extrinsic fields (x, y). Tests the basic factory-cache pattern and measuring hit rates. The cache should have ~10 entries for 100,000 dots. |
| Easy | Icon Library Flyweight A file manager displays icons next to file names. Each icon has a type ( "pdf", "jpg", "folder", etc.) and an image (expensive to load, ~50 KB each). The file manager shows 10,000 files but only ~20 distinct file types. Build an Icon flyweight, an IconFactory, and a FileEntry that stores (name, path) + a reference to the shared icon. Count how many icons are loaded vs. how many files are displayed. | Tests Flyweight with a heavy intrinsic payload (image data). The 50 KB icon is loaded once per type, not once per file. 20 icons ร 50 KB = 1 MB vs. 10,000 ร 50 KB = 500 MB. Demonstrates why Flyweight is critical for resource-heavy shared objects. |
| Medium | Text Editor Character Styles Build the text editor from Sections 1–4. A CharStyle flyweight stores (font, size, colour, bold, italic). A CharStyleFactory caches unique styles. A Document holds a list of (char, row, col, CharStyle) entries. Implement render() that prints each character using its shared style, and memoryReport() that compares actual flyweight count to total character count. Parse a sample text with 3 styles: body, heading, and code. | Tests a multi-field intrinsic key (5 fields must all match for sharing). The factory key is a composite of all intrinsic fields. Also tests the full lifecycle: parse text โ look up styles โ build document โ render โ report memory. Real-world applicable to any rich-text renderer. |
| Medium | Particle System Flyweight A game particle system emits effects: Fire, Smoke, Spark. Each particle type has a texture, colour gradient, and physics profile (intrinsic โ shared by all particles of that type). Each particle instance has position (x, y, z), velocity, and remaining lifetime (extrinsic โ unique per particle). Build a ParticleType flyweight, a factory, and a Particle context. Implement a ParticleSystem that emits 50,000 particles across 3 types and updates their positions each frame. Show the cache has only 3 entries. | Tests Flyweight in a high-throughput scenario (particles updated every frame). The extrinsic state changes each frame (position/velocity are mutable), but the intrinsic state (texture/physics) is shared and immutable. Demonstrates that extrinsic state can be mutable while intrinsic must not be. |
| Hard | Map Tile Renderer with Bounded Cache A map application renders terrain tiles. Each tile has a terrain type (intrinsic: biome name, texture, base colour, movement cost) and a grid position (extrinsic: row, col). The world is 10,000 × 10,000 but uses ~50 terrain types. Twist: implement the factory with a bounded LRU cache (max 30 entries) that evicts the least-recently-used flyweight when full. Track cache hits, misses, and evictions. Also implement thread-safe access using ConcurrentHashMap so multiple renderer threads can request flyweights simultaneously. | Tests an advanced flyweight with: (1) bounded cache with LRU eviction (what happens when the cache is smaller than the unique count?), (2) thread-safety for concurrent access, (3) cache statistics (hit rate, miss rate, eviction count). This mirrors real-world tile engines and CDN caches where memory is limited and cache management is critical. |
- When asked about Flyweight, the interviewer wants to see: (1) a clear split between intrinsic (shared, immutable) and extrinsic (unique, passed in) state — this is the core design decision; (2) a factory with a cache that guarantees sharing; (3) the flyweight is immutable (records are perfect); (4) awareness that
Integer.valueOf(),String.intern(), andBoolean.TRUE/FALSEare JVM flyweights. - Stand-out answers mention cache eviction strategies for unbounded intrinsic combinations,
ConcurrentHashMapfor thread-safety, and that Flyweight is a memory optimisation (measure first, then apply).