Loading learning content...
Consider a public library. Instead of every household owning every book they might read, libraries share books across thousands of patrons. A single copy of 'War and Peace' serves hundreds of readers over its lifetime. Each reader adds their own context—which page they're on, their notes, their reading schedule—but the book itself is shared.\n\nThe Flyweight Pattern applies this same principle to software objects. Instead of creating 50,000 character objects each containing 'Arial 12pt Black', we create one flyweight object representing 'Arial 12pt Black' and share it across all 50,000 characters. Each character maintains only its unique identity—the character itself and its position—referencing the shared flyweight for everything else.\n\nThis transformation is both conceptually simple and practically powerful. Memory consumption drops from megabytes to kilobytes. GC pressure vanishes for shared components. And the code often becomes cleaner, as shared state naturally gravitates to its own cohesive abstraction.
By the end of this page, you will understand the Flyweight Pattern's complete solution architecture: how to extract shared state into flyweight objects, how the flyweight factory manages unique instances, how clients interact with flyweights while providing extrinsic state, and the precise participants that make up the pattern's structure.
The Flyweight Pattern rests on a subtle but profound distinction: the difference between what an object is and what an object has.\n\nTraditional OOP Thinking:\nEach object is a unique entity with its own identity and its own complete state. Two objects representing 'Arial 12pt Black' are distinct objects, even if they contain identical data.\n\nFlyweight Thinking:\nObjects exist conceptually, but their physical representation is factored. The shareable aspects (intrinsic state) are consolidated into one shared object. The unique aspects (extrinsic state) are stored elsewhere or computed on demand.\n\nThis shifts us from object-per-concept to object-per-unique-state-combination.
The Mathematical Reality:\n\nWith Flyweight applied to our 50,000 characters using 3 unique styles:\n- Flyweight objects: 3 × 100 bytes = 300 bytes\n- Context data per character: ~12 bytes (char + position)\n- Context for all characters: 50,000 × 12 = 600,000 bytes = ~600 KB\n\nTotal: ~600 KB instead of ~5 MB — an 88% reduction\n\nAnd because we only have 3 long-lived flyweight objects instead of 50,000 ephemeral character objects, GC overhead drops to nearly zero for the shared components.
The Flyweight Pattern has a precise structure with clearly defined participants. Understanding each participant's role is essential for correct implementation.
The FlyweightFactory is the pattern's central mechanism. It serves as a controlled access point for flyweight objects, implementing a simple but crucial caching strategy:\n\n1. Receive a request for a flyweight with specific intrinsic state\n2. Check if a flyweight with that state already exists\n3. If yes: return the existing flyweight\n4. If no: create a new flyweight, cache it, and return it\n\nThis ensures that each unique combination of intrinsic state exists exactly once in memory.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
import java.util.Map;import java.util.concurrent.ConcurrentHashMap; /** * The Flyweight interface declares operations that accept extrinsic state. * Flyweights are stateless with respect to extrinsic concerns. */public interface CharacterStyle { /** * Render a character using this style at the given position. * The character and position are extrinsic state—they vary per use. * The font, size, color are intrinsic—shared across all uses. */ void render(char character, int x, int y, Canvas canvas); /** * Calculate the width of a character in this style. * Width depends on intrinsic state (font, size) plus the character. */ int getCharWidth(char character);} /** * ConcreteFlyweight: stores only intrinsic, shareable state. * * This class is immutable—once created, its state never changes. * Multiple clients can safely share the same instance. */public final class CharacterStyleImpl implements CharacterStyle { // Intrinsic state: shared across all characters using this style private final String fontFamily; private final int fontSize; private final Color textColor; private final boolean bold; private final boolean italic; private final boolean underline; // Cached font metrics for performance private final FontMetrics metrics; public CharacterStyleImpl( String fontFamily, int fontSize, Color textColor, boolean bold, boolean italic, boolean underline) { this.fontFamily = fontFamily; this.fontSize = fontSize; this.textColor = textColor; this.bold = bold; this.italic = italic; this.underline = underline; // Pre-compute font metrics for this style this.metrics = FontMetrics.forStyle(fontFamily, fontSize, bold, italic); } @Override public void render(char character, int x, int y, Canvas canvas) { // Use intrinsic state (font, color, style) to render // The character and position are extrinsic—passed in canvas.setFont(fontFamily, fontSize, bold, italic); canvas.setColor(textColor); canvas.drawChar(character, x, y); if (underline) { int width = getCharWidth(character); canvas.drawLine(x, y + 2, x + width, y + 2); } } @Override public int getCharWidth(char character) { return metrics.getWidth(character); } // Intrinsic state is immutable—no setters provided public String getFontFamily() { return fontFamily; } public int getFontSize() { return fontSize; } public Color getTextColor() { return textColor; } public boolean isBold() { return bold; } public boolean isItalic() { return italic; } public boolean isUnderline() { return underline; }}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
/** * FlyweightFactory: creates and manages flyweight instances. * * The factory ensures each unique combination of intrinsic state * exists exactly once in memory. It acts as a cache/registry. */public final class CharacterStyleFactory { // Cache of existing flyweight instances // Key: composite of all intrinsic state properties private final Map<String, CharacterStyle> styleCache; // Singleton factory (optional, but common) private static final CharacterStyleFactory INSTANCE = new CharacterStyleFactory(); private CharacterStyleFactory() { // ConcurrentHashMap for thread-safe access this.styleCache = new ConcurrentHashMap<>(); } public static CharacterStyleFactory getInstance() { return INSTANCE; } /** * Get or create a flyweight for the given intrinsic state. * * If a flyweight with this exact state already exists, it's returned. * Otherwise, a new flyweight is created, cached, and returned. */ public CharacterStyle getStyle( String fontFamily, int fontSize, Color textColor, boolean bold, boolean italic, boolean underline) { // Create a unique key from intrinsic state String key = createKey(fontFamily, fontSize, textColor, bold, italic, underline); // ComputeIfAbsent: thread-safe get-or-create return styleCache.computeIfAbsent(key, k -> { System.out.println("Creating new flyweight: " + key); return new CharacterStyleImpl( fontFamily, fontSize, textColor, bold, italic, underline ); }); } /** * Convenience method for common style combinations. */ public CharacterStyle getDefaultStyle() { return getStyle("Arial", 12, Color.BLACK, false, false, false); } public CharacterStyle getBoldStyle(String fontFamily, int fontSize) { return getStyle(fontFamily, fontSize, Color.BLACK, true, false, false); } /** * Create a unique cache key from intrinsic state. */ private String createKey( String fontFamily, int fontSize, Color textColor, boolean bold, boolean italic, boolean underline) { return String.format("%s_%d_%d_%b_%b_%b", fontFamily, fontSize, textColor.getRGB(), bold, italic, underline); } /** * Get statistics about flyweight reuse. */ public int getFlyweightCount() { return styleCache.size(); } /** * Clear cache (useful for testing or memory pressure scenarios). */ public void clearCache() { styleCache.clear(); }}The cache key must uniquely identify all intrinsic state. If two different combinations hash to the same key, you'll share the wrong flyweight. Typically, keys are composite strings or hash combinations of all intrinsic fields. For complex intrinsic state, consider implementing equals() and hashCode() properly and using the flyweight itself as the map key.
The client's responsibility in the Flyweight Pattern is twofold:\n\n1. Obtain flyweights from the factory based on needed intrinsic state\n2. Manage and provide extrinsic state when invoking flyweight operations\n\nThe client essentially acts as the 'glue' that binds shared flyweights to their specific usage contexts.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
/** * Client: manages extrinsic state and uses flyweights. * * This document renderer stores characters with their positions * (extrinsic state) and references to shared styles (flyweights). */public class DocumentRenderer { private final CharacterStyleFactory styleFactory; private final List<CharacterEntry> characters; /** * Lightweight container for extrinsic state + flyweight reference. * This is what we store per character instead of full objects. */ private static class CharacterEntry { final char character; // Extrinsic: the actual character final int x, y; // Extrinsic: position on page final CharacterStyle style; // Reference to shared flyweight CharacterEntry(char c, int x, int y, CharacterStyle style) { this.character = c; this.x = x; this.y = y; this.style = style; } // Memory: ~16-20 bytes vs ~100+ bytes for full FormattedCharacter } public DocumentRenderer() { this.styleFactory = CharacterStyleFactory.getInstance(); this.characters = new ArrayList<>(); } /** * Load a document, creating flyweight references for each character. */ public void loadDocument(Document document) { characters.clear(); for (Page page : document.getPages()) { int pageOffsetY = page.getNumber() * page.getHeight(); for (TextRun textRun : page.getTextRuns()) { // Get the flyweight for this text run's style // The factory returns a cached instance if available CharacterStyle style = styleFactory.getStyle( textRun.getFontFamily(), textRun.getFontSize(), textRun.getColor(), textRun.isBold(), textRun.isItalic(), textRun.isUnderlined() ); // Store each character with position + flyweight ref int x = textRun.getX(); int y = textRun.getY() + pageOffsetY; String text = textRun.getText(); for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); characters.add(new CharacterEntry(c, x, y, style)); x += style.getCharWidth(c); } } } // Log flyweight efficiency System.out.printf("Loaded %,d characters using only %d flyweights%n", characters.size(), styleFactory.getFlyweightCount()); } /** * Render all characters to the canvas. */ public void render(Canvas canvas) { for (CharacterEntry entry : characters) { // Client provides extrinsic state (char, position) to flyweight // Flyweight uses its intrinsic state (font, color) to render entry.style.render(entry.character, entry.x, entry.y, canvas); } } /** * Memory usage comparison. */ public void printMemoryStats() { int charCount = characters.size(); int flyweightCount = styleFactory.getFlyweightCount(); // Naive approach: ~100 bytes per character long naiveMemory = charCount * 100L; // Flyweight approach: ~20 bytes per entry + ~100 bytes per flyweight long flyweightMemory = (charCount * 20L) + (flyweightCount * 100L); System.out.printf("Characters: %,d%n", charCount); System.out.printf("Unique styles: %d%n", flyweightCount); System.out.printf("Naive memory: %,.0f KB%n", naiveMemory / 1024.0); System.out.printf("Flyweight memory: %,.0f KB%n", flyweightMemory / 1024.0); System.out.printf("Memory saved: %.1f%%%n", (1 - (double)flyweightMemory / naiveMemory) * 100); }}Sample Output:\n\n\nCreating new flyweight: Arial_12_-16777216_false_false_false\nCreating new flyweight: Arial_12_-16777216_true_false_false\nCreating new flyweight: Courier_10_-16777216_false_false_false\nLoaded 87,432 characters using only 3 flyweights\n\nCharacters: 87,432\nUnique styles: 3\nNaive memory: 8,534 KB\nFlyweight memory: 1,706 KB\nMemory saved: 80.0%\n\n\nWith just 3 unique text styles across nearly 90,000 characters, we achieve an 80% memory reduction. In documents with more uniform styling, savings approach 90%+ routinely.
Let's visualize the complete transformation from naive object creation to efficient flyweight sharing.
| Aspect | Naive Approach | Flyweight Approach |
|---|---|---|
| Objects for 5 chars | 5 full objects | 1 flyweight + 5 entries |
| Bytes per char | ~100 bytes | ~20 bytes (entry) + shared flyweight |
| Total for 5 chars | ~500 bytes | ~100 + 100 = ~200 bytes |
| For 50,000 chars | ~5 MB | ~1 MB (or less) |
| GC objects | 50,000 objects | 1-10 flyweights + lightweight entries |
| Shared state | Duplicated 50,000× | Stored once per unique style |
When flyweights are shared across threads—common in server applications and multi-threaded rendering—thread safety becomes crucial. The good news: properly designed flyweights are inherently thread-safe because they're immutable.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
/** * Thread-safe flyweight factory implementation. */public final class ThreadSafeStyleFactory { // ConcurrentHashMap provides thread-safe operations private final ConcurrentHashMap<String, CharacterStyle> cache = new ConcurrentHashMap<>(); /** * Thread-safe flyweight retrieval using computeIfAbsent. * * computeIfAbsent is atomic: only one thread creates the flyweight * for a given key, even under concurrent access. */ public CharacterStyle getStyle(StyleKey key) { return cache.computeIfAbsent(key.toString(), k -> { // This lambda executes at most once per unique key return new CharacterStyleImpl( key.fontFamily, key.fontSize, key.color, key.bold, key.italic, key.underline ); }); } // Alternative: explicit synchronization for complex creation logic private final Object creationLock = new Object(); public CharacterStyle getStyleWithComplexInit(StyleKey key) { String cacheKey = key.toString(); // Fast path: no synchronization for existing flyweights CharacterStyle existing = cache.get(cacheKey); if (existing != null) { return existing; } // Slow path: synchronized creation synchronized (creationLock) { // Double-check after acquiring lock existing = cache.get(cacheKey); if (existing != null) { return existing; } // Complex initialization (e.g., loading fonts from disk) CharacterStyle newStyle = createWithComplexSetup(key); cache.put(cacheKey, newStyle); return newStyle; } } private CharacterStyle createWithComplexSetup(StyleKey key) { // Load font metrics, validate characters, etc. return new CharacterStyleImpl( key.fontFamily, key.fontSize, key.color, key.bold, key.italic, key.underline ); }}If a flyweight is modified after creation, all clients sharing that flyweight see the change—almost certainly not the intended behavior. Make flyweights immutable: use final fields, avoid setters, and never store mutable extrinsic state within flyweights.
In long-running applications, flyweight lifecycle becomes important. The factory's cache keeps flyweights alive indefinitely—desirable for frequently-used styles but potentially wasteful for rarely-used ones.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
import java.lang.ref.WeakReference;import java.util.WeakHashMap; /** * Flyweight factory using weak references for automatic cleanup. * * When no external references to a flyweight exist, it becomes * eligible for garbage collection—freeing memory automatically. */public class WeakFlyweightFactory { // WeakHashMap: values are weakly referenced // Note: For complex scenarios, consider a proper cache like Caffeine private final Map<StyleKey, WeakReference<CharacterStyle>> cache = new WeakHashMap<>(); public synchronized CharacterStyle getStyle(StyleKey key) { WeakReference<CharacterStyle> ref = cache.get(key); CharacterStyle style = (ref != null) ? ref.get() : null; if (style == null) { // Flyweight was collected or doesn't exist—recreate style = new CharacterStyleImpl( key.fontFamily, key.fontSize, key.color, key.bold, key.italic, key.underline ); cache.put(key, new WeakReference<>(style)); } return style; } // Alternatively, use a bounded cache with eviction public static FlyweightFactory createBoundedFactory(int maxSize) { return new LRUFlyweightFactory(maxSize); }} /** * Alternative: LRU-bounded flyweight factory. */class LRUFlyweightFactory { private final LinkedHashMap<StyleKey, CharacterStyle> cache; private final int maxSize; LRUFlyweightFactory(int maxSize) { this.maxSize = maxSize; this.cache = new LinkedHashMap<>(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > LRUFlyweightFactory.this.maxSize; } }; } public synchronized CharacterStyle getStyle(StyleKey key) { return cache.computeIfAbsent(key, k -> new CharacterStyleImpl( k.fontFamily, k.fontSize, k.color, k.bold, k.italic, k.underline ) ); }}We've thoroughly examined the Flyweight Pattern's solution architecture. Let's consolidate the key principles:
What's Next:\n\nWith the solution architecture clear, we need to deeply understand the distinction between intrinsic and extrinsic state. This separation is the intellectual core of the Flyweight Pattern—get it right, and the pattern works beautifully; get it wrong, and you'll face subtle bugs and incomplete memory savings. The next page explores this critical distinction in depth.
You now understand how the Flyweight Pattern transforms massive object collections into efficient shared structures. The factory, flyweights, and client interaction form a cohesive system that achieves memory reductions of 80-95% in typical scenarios while simplifying the conceptual model of your data.