Structural

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.

Overview ยท Structural ยท Singleton ยท Factory Method ยท Abstract Factory ยท Builder ยท Prototype ยท Adapter ยท Bridge ยท Composite ยท Decorator ยท Facade ยท Flyweight ยท Proxy ยท Observer ยท Strategy ยท Command ยท Template Method ยท Chain of Resp. ยท State ยท Mediator ยท Iterator ยท Visitor ยท Memento ยท Interpreter
Category: Structural Difficulty: Advanced Interview: Tier 3 Confused with: Prototype
01
Section One ยท The Problem

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.

Naive approach — every character is a full object
// โœ— Each character carries ALL state โ€” shared + unique class Character { final char codePoint; // unique per char final String fontFamily; // "Arial" โ€” same for 10,000 chars final int fontSize; // 12 โ€” same for 10,000 chars final Color color; // BLACK โ€” same for 10,000 chars final boolean bold; // false โ€” same for 10,000 chars final int row, col; // unique per char } // โœ— 100,000 characters ร— ~200 bytes = 20 MB // โœ— 95% of that memory stores duplicated font/colour data

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.
Without Flyweight — duplicated intrinsic state
Character #1 ('H') font: "Arial" size: 12, color: BLACK bold: false row: 0, col: 0 Character #2 ('e') font: "Arial" size: 12, color: BLACK bold: false row: 0, col: 1 Character #3 ('l') font: "Arial" size: 12, color: BLACK bold: false row: 0, col: 2 โ€ฆ Character #100,000 font: "Arial" size: 12, color: BLACK bold: false row: 2047, col: 38 ✗ font, size, colour, bold duplicated 100,000 times ✗ ~200 bytes ร— 100,000 = 20 MB for text alone ✗ Only row/col differ — 95% of memory is wasted copies ✗ GC must track 100,000 objects

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.

02
Section Two ยท The Pattern

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.

GoF Intent: “Use sharing to support large numbers of fine-grained objects efficiently.”
— Gamma, Helm, Johnson, Vlissides (1994)
Analogy — movable type printing press: Before Gutenberg, every page was carved from a single block of wood — one unique block per page. Gutenberg’s insight: individual letter blocks (A, B, C…) are reusable. The letter ‘e’ appears thousands of times in a book, but you don’t carve thousands of ‘e’ blocks. You cast a few ‘e’ blocks and reuse them in different positions on different pages. The letter shape (intrinsic state) is shared; the position on the page (extrinsic state) changes every time. That’s Flyweight — share the common part, supply the unique part at use time.

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
Identify which state is intrinsic (shared, immutable) and which is extrinsic (unique, context-dependent)
This split is the hardest design decision — get it wrong and either sharing is impossible or the API becomes awkward.
Store intrinsic state in a shared Flyweight object. Make it immutable.
10,000 characters with “Arial 12pt Black” all reference one CharStyle flyweight instead of 10,000 copies.
A FlyweightFactory acts as a cache: if a flyweight with the requested intrinsic state already exists, return it; otherwise create and cache it.
Guarantees sharing. The factory is the single point of creation — clients never call new Flyweight() directly.
The client passes extrinsic state (row, col) to the flyweight’s operation method at use time
The flyweight combines shared intrinsic state + supplied extrinsic state to perform the operation (e.g. render(row, col)). No duplication.
With Flyweight — shared intrinsic state, extrinsic passed in
CharStyle flyweight (shared) font: "Arial" | size: 12 color: BLACK | bold: false โ†‘ stored ONCE, shared by all 'H' @ (0,0) extrinsic only 'e' @ (0,1) extrinsic only 'l' @ (0,2) extrinsic only โ€ฆ 'z' @ (2047,38) extrinsic only ✔ 100,000 characters share 1 flyweight โ†’ ~20 KB instead of 20 MB ✔ Extrinsic state (position) stored outside; intrinsic state (style) stored once
Flyweight vs. Caching vs. Singleton:
  • 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.
03
Section Three ยท Anatomy

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.
Flyweight — UML Class Diagram
«interface» CharStyle + render(c, row, col) : void ConcreteCharStyle - font : String intrinsic - size : int intrinsic - color : Color intrinsic - bold : boolean intrinsic CharStyleFactory - cache : Map<Key, CharStyle> + getStyle(font,size,โ€ฆ) : CharStyle creates TextEditor asks for uses (passes extrinsic) Extrinsic State char c (the character) int row, col (position) โ†‘ NOT stored in flyweight โ€” passed by client passed to ■ Blue = Flyweight interface ■ Green = ConcreteFlyweight (shared, immutable) ■ Gold = FlyweightFactory (cache + creation) □ Red dashed = Extrinsic state (not stored in flyweight) Client asks factory for a flyweight, then passes extrinsic state to the flyweight’s operation at call time
The factory is essential:
  • Without the CharStyleFactory, clients would create flyweights directly with new, and sharing would be accidental.
  • The factory is the single point of creation that enforces sharing.
  • It’s typically implemented with a HashMap keyed 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.
04
Section Four ยท How It Works

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.

Step 1 — Editor parses first character: ‘H’ with style Arial/12/Black/normal at position (0, 0)
Editor calls factory.getStyle("Arial", 12, BLACK, false). Factory checks its cache — empty. Creates a new CharStyle, caches it, returns it. Cache size: 1.
Step 2 — Editor parses next 9,999 characters: all same style (Arial/12/Black/normal)
Each calls 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.
Step 3 — Editor hits a bold heading: ‘T’, ‘i’, ‘t’, ‘l’, ‘e’ with style Arial/18/Black/bold
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.
Step 4 — Editor renders the document: iterates all characters, calling style.render(c, row, col)
Each character passes its extrinsic state (char code, row, column) to the shared flyweight’s render() method. The flyweight combines its intrinsic state (font/size/colour) with the supplied position โ†’ draws the character on screen.
Result — 10,005 characters, only 2 flyweight objects in memory
Without Flyweight: 10,005 full objects ร— ~200 bytes = ~2 MB. With Flyweight: 2 style objects + 10,005 lightweight position entries โ‰ˆ ~80 KB. 96% memory reduction.
Flyweight — Factory Cache Lifecycle
char #1 getStyle(Arial,12, BLACK,false) MISS โ†’ create chars #2โ€“10,000 getStyle(Arial,12, BLACK,false) HIT โ†’ reuse ร—9,999 chars #10,001โ€“10,005 getStyle(Arial,18, BLACK,true) MISS โ†’ create render all style.render(c,row,col) for each character extrinsic passed in Factory Cache Key: (Arial,12,BLACK,false) โ†’ CharStyle@a1 Key: (Arial,18,BLACK,true)  โ†’ CharStyle@b2 Memory Comparison Without flyweight: 10,005 ร— 200B โ‰ˆ 2 MB With flyweight: 2 styles + 10,005 ร— 8B โ‰ˆ 80 KB (96% less) 10,005 characters created โ†’ only 2 cache misses โ†’ 2 flyweight objects total Each character stores: char + row + col + reference to shared flyweight (8 bytes) โœ“ The flyweight is immutable โ€” safe to share across all 10,005 references
The pattern in pseudocode
// โ”€โ”€ Flyweight (immutable, shared) โ”€โ”€ record CharStyle(String font, int size, Color color, boolean bold) { void render(char c, int row, int col) { // uses intrinsic (font, size, color, bold) + extrinsic (c, row, col) System.out.printf("Draw '%c' at (%d,%d) in %s %dpt %s%n", c, row, col, font, size, bold ? "BOLD" : "normal"); } } // โ”€โ”€ FlyweightFactory (cache) โ”€โ”€ class CharStyleFactory { private final Map<CharStyle, CharStyle> cache = new HashMap<>(); CharStyle getStyle(String font, int size, Color color, boolean bold) { CharStyle key = new CharStyle(font, size, color, bold); return cache.computeIfAbsent(key, k -> k); // return cached or store new } } // โ”€โ”€ Client โ”€โ”€ CharStyleFactory factory = new CharStyleFactory(); CharStyle body = factory.getStyle("Arial", 12, Color.BLACK, false); CharStyle heading = factory.getStyle("Arial", 18, Color.BLACK, true); body.render('H', 0, 0); // extrinsic state passed in body.render('e', 0, 1); heading.render('T', 5, 0); // body == factory.getStyle("Arial", 12, BLACK, false) โ†’ same instance โœ“
Why records work perfectly here:
  • 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 perfect HashMap keys for the factory cache.
  • Using records for flyweights is a modern Java best practice.
05
Section Five ยท Java Stdlib

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.

IN JAVA
Example 1 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.
Example 2 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).
Example 3 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.
Example 4 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.
Stdlib usage — Integer cache as Flyweight
// โ”€โ”€ The JVM's built-in flyweight cache โ”€โ”€ Integer a = Integer.valueOf(42); // cache hit โ†’ returns cached instance Integer b = Integer.valueOf(42); // cache hit โ†’ same instance System.out.println(a == b); // true โ€” same flyweight object Integer c = Integer.valueOf(200); // outside -128..127 โ†’ new object Integer d = Integer.valueOf(200); // outside range โ†’ another new object System.out.println(c == d); // false โ€” different objects, no flyweight // โ”€โ”€ String pool: the original flyweight โ”€โ”€ String s1 = "hello"; // literal โ†’ interned in string pool String s2 = "hello"; // same literal โ†’ same pooled instance System.out.println(s1 == s2); // true โ€” same flyweight String s3 = new String("hello"); // bypasses pool โ†’ new object System.out.println(s1 == s3); // false โ€” not the pooled flyweight System.out.println(s1 == s3.intern()); // true โ€” intern() returns the pool instance
Under the hood — Integer.valueOf() flyweight factory
// Simplified version of java.lang.Integer.valueOf() public static Integer valueOf(int i) { if (i >= -128 && i <= 127) { return IntegerCache.cache[i + 128]; // โ† flyweight from cache } return new Integer(i); // no flyweight for large values } // The cache is populated at class-load time: private static class IntegerCache { static final Integer[] cache; static { cache = new Integer[256]; // -128 to 127 for (int i = 0; i < 256; i++) cache[i] = new Integer(i - 128); } }
Autoboxing hides the flyweight:
  • When you write Integer x = 42;, the compiler auto-inserts Integer.valueOf(42), which uses the flyweight cache.
  • This is why two autoboxed int values 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) return false but Integer.valueOf(5) == Integer.valueOf(5) return true?”
06
Section Six ยท Implementation

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.

Java — Flyweight Pattern Game Forest (core)
// โ”€โ”€ Flyweight (immutable, shared intrinsic state) โ”€โ”€ record TreeType(String name, String texture, String color) { void render(int x, int y) { System.out.printf(" ๐ŸŒฒ %s [%s/%s] at (%d, %d)%n", name, texture, color, x, y); } } // โ”€โ”€ FlyweightFactory โ”€โ”€ class TreeTypeFactory { private static final Map<String, TreeType> cache = new HashMap<>(); static TreeType get(String name, String texture, String color) { String key = name + "|" + texture + "|" + color; return cache.computeIfAbsent(key, k -> new TreeType(name, texture, color)); } static int cacheSize() { return cache.size(); } } // โ”€โ”€ Context (extrinsic state: position) โ”€โ”€ record Tree(int x, int y, TreeType type) { void render() { type.render(x, y); } }
The record + computeIfAbsent combo:
  • In the Java implementation, TreeType is a record (immutable, auto equals/hashCode) and the factory uses Map.computeIfAbsent() for atomic get-or-create.
  • This two-line factory is the idiomatic modern Java way to implement Flyweight.
  • For thread-safety, swap HashMap for ConcurrentHashMapcomputeIfAbsent is already atomic on ConcurrentHashMap.
07
Section Seven ยท Watch Out

Common Mistakes

Mistake #1 — Making the flyweight mutable: If the flyweight’s intrinsic state can be modified, one client’s change affects every client sharing that flyweight. This causes horrifying bugs: change one tree’s colour and every oak in the forest turns red. Fix: Make flyweights immutable — use Java records, final fields, no setters. If you need different state, request a different flyweight from the factory.
✗ Wrong — mutable flyweight shared by many objects
// โœ— Mutable flyweight โ€” disaster waiting to happen class TreeType { String name; String color; // โœ— not final! void setColor(String c) { this.color = c; } // โœ— mutates shared state! } // Client changes one "oak" type's colour โ†’ ALL 500 oaks turn red oakType.setColor("#FF0000"); // โœ— affects every tree sharing this flyweight
✔ Correct — immutable flyweight (record)
// โœ“ Immutable record โ€” safe to share record TreeType(String name, String texture, String color) { // No setters. All fields final. Cannot be modified after creation. // Want a red oak? Get a DIFFERENT flyweight: factory.get("Oak", tex, "#FF0000") }
Mistake #2 — Storing extrinsic state inside the flyweight: If the flyweight stores position, context, or any per-use data, it can’t be shared — every user needs a unique instance, defeating the entire purpose. Test: if removing a field from the flyweight means different users can share the same instance, that field is extrinsic and must be passed in as a parameter.
✗ Wrong — extrinsic state stored in flyweight
// โœ— Position stored inside flyweight โ€” can't share across trees record TreeType(String name, String texture, String color, int x, int y) { // โœ— x,y are extrinsic! // Every tree at a different position = unique flyweight = no sharing }
✔ Correct — extrinsic state passed as parameter
// โœ“ Only intrinsic state in flyweight; extrinsic passed at call time record TreeType(String name, String texture, String color) { void render(int x, int y) { // โœ“ x,y passed in โ€” not stored // combine intrinsic (name, texture, color) + extrinsic (x, y) } }
Mistake #3 — Using Flyweight when there’s no sharing opportunity:
  • 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.
Mistake #4 — Unbounded cache growth:
  • 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 (LinkedHashMap with removeEldestEntry), a WeakHashMap, or Caffeine/Guava cache with eviction policies. Monitor cache size in production.
08
Section Eight ยท Decision Guide

When To Use Flyweight

Use Flyweight When
  • 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)
Avoid Flyweight When
  • 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
Flyweight vs. Related Patterns
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
Decision Flowchart
Thousands of similar objects in memory? Yes No No pattern needed Most state shared and immutable? No Prototype / Pool Yes High cache hit rate (few unique combos)? No Cache / memoize Yes Flyweight
09
Section Nine ยท Practice

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.
Interview Tip:
  • 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(), and Boolean.TRUE/FALSE are JVM flyweights.
  • Stand-out answers mention cache eviction strategies for unbounded intrinsic combinations, ConcurrentHashMap for thread-safety, and that Flyweight is a memory optimisation (measure first, then apply).