Chapter 04 · Article 20 of 55
Flyweight Pattern
Intent: Use sharing to support large numbers of fine-grained objects efficiently.
Article outline16 sections on this page+
Overview
Intent: Use sharing to support large numbers of fine-grained objects efficiently.
Pattern Type: Structural
GoF Classification: Object Structural
Complexity: **
Frequency of Use: * (high in performance-critical systems)
The Flyweight pattern minimizes memory usage by sharing as much data as possible between similar objects. Instead of each object storing all its own data, common (intrinsic) state is extracted into shared flyweight objects, while unique (extrinsic) state is kept outside and passed in when needed.
The name "flyweight" comes from boxing - the lightest weight class - reflecting the pattern's goal of making objects as lightweight as possible.
Problem It Solves
Consider a text editor rendering a million-character document. A naive approach creates one object per character, each storing:
- The character value
- Font family, size, weight
- Color
- Position (x, y)
- Paragraph reference
With 1,000,000 characters × ~100 bytes each = ~100 MB of memory for character objects alone. Most of this is redundant - a document typically uses only a handful of distinct font/style combinations.
Core problems addressed:
- Memory bloat from millions of similar objects with duplicated state
- Redundant state storage where thousands of objects share identical field values
- Object creation overhead when instantiating near-identical objects repeatedly
- GC pressure from massive object counts in managed-memory languages
- Cache thrashing when working sets exceed CPU cache capacity
The Flyweight pattern solves this by recognizing that most "per-object" state is actually shared across many objects and can be factored out.
When to Use / When NOT to Use
| When to Use | When NOT to Use |
|---|---|
| Application uses a large number of similar objects | Few objects exist in the system |
| Storage costs are high due to sheer object quantity | Objects have mostly unique state |
| Most object state can be made extrinsic | Extrinsic state cannot be easily computed or passed |
| Groups of objects can be replaced by fewer shared objects + extrinsic state | Object identity matters (each must be distinct) |
| Application doesn't depend on object identity (== vs equals) | Shared state is mutable and requires synchronization |
| Memory profiling confirms redundant state as the bottleneck | Complexity of separating state outweighs memory savings |
Key Concepts & Theory
Intrinsic State (Shared, Immutable)
State that is independent of the flyweight's context - it remains constant regardless of where or how the flyweight is used. This state lives inside the flyweight object and is shared across all contexts.
- Must be immutable (thread-safe by nature)
- Stored once, referenced many times
- Examples: character glyph shape, tree species texture, icon bitmap data
Extrinsic State (Context-Dependent, Passed In)
State that varies with context and cannot be shared. The client computes or stores this state and passes it to the flyweight's methods at operation time.
- Stored externally (in the client or a separate structure)
- Passed as method parameters
- Examples: character position on screen, tree coordinates in forest, icon tooltip text
Flyweight Factory
A creation mechanism that manages the flyweight pool. When a client requests a flyweight:
- Check if a matching flyweight already exists in the pool
- If yes → return the existing shared instance
- If no → create a new flyweight, add to pool, return it
This ensures flyweights are shared rather than duplicated.
Connection to Object Pooling
Flyweight and Object Pool both reuse objects, but differ in intent:
- Flyweight: Shares immutable objects simultaneously across multiple clients (concurrent sharing)
- Object Pool: Lends mutable objects to one client at a time (exclusive borrowing)
A Flyweight Factory is a specialized pool where objects are never "returned" - they're shared concurrently because their immutability makes this safe.
ASCII Class Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ CLIENT │
│ │
│ - Computes/stores extrinsic state │
│ - Requests flyweights from factory │
│ - Passes extrinsic state to flyweight operations │
└──────────────────────┬──────────────────────────────────────────────┘
│ requests flyweight
▼
┌─────────────────────────────────────────┐
│ FlyweightFactory │
│─────────────────────────────────────────│
│ - pool: Map<Key, Flyweight> │
│─────────────────────────────────────────│
│ + getFlyweight(key): Flyweight │
│ [if exists in pool → return cached] │
│ [if not → create, cache, return] │
└──────────────────────┬──────────────────┘
│ creates/returns
▼
┌─────────────────────────────┐
│ <<interface>> Flyweight │
│─────────────────────────────│
│ + operation(extrinsicState) │
└──────────────┬──────────────┘
│
┌────────────┴────────────┐
▼ ▼
┌───────────────────┐ ┌────────────────────────┐
│ ConcreteFlyweight │ │ UnsharedConcreteFlyweight│
│───────────────────│ │────────────────────────│
│ - intrinsicState │ │ - allState │
│ (shared, │ │ (not shared) │
│ immutable) │ │ │
│───────────────────│ │────────────────────────│
│ + operation( │ │ + operation( │
│ extrinsic) │ │ extrinsic) │
└───────────────────┘ └────────────────────────┘
Data flow:
Client ──[extrinsic state]──► Flyweight.operation(position, context)
│
uses intrinsic state (font, glyph)
+ received extrinsic state (position)
│
▼
Renders character at position with shared style
Pseudocode Implementation
Example 1: Text Editor Character Rendering
// Flyweight - stores intrinsic state only
class CharacterFlyweight:
// Intrinsic state: shared across all characters with same style
private font: String // e.g., "Arial"
private size: int // e.g., 12
private weight: String // e.g., "bold"
private color: Color // e.g., BLACK
constructor(font, size, weight, color):
this.font = font
this.size = size
this.weight = weight
this.color = color
// Extrinsic state (position) passed in at operation time
method render(character: char, x: int, y: int):
drawGlyph(character, this.font, this.size, this.weight, this.color, x, y)
// Flyweight Factory - manages the pool
class CharacterStyleFactory:
private pool: Map<String, CharacterFlyweight> = {}
method getStyle(font, size, weight, color) -> CharacterFlyweight:
key = font + "_" + size + "_" + weight + "_" + color.toString()
if key NOT in pool:
pool[key] = new CharacterFlyweight(font, size, weight, color)
return pool[key]
method getPoolSize() -> int:
return pool.size()
// Client - stores extrinsic state, uses flyweights
class TextDocument:
private characters: List<{char, styleKey, x, y}> = []
private factory: CharacterStyleFactory = new CharacterStyleFactory()
method addCharacter(char, font, size, weight, color, x, y):
style = factory.getStyle(font, size, weight, color)
characters.add({char, style, x, y})
method render():
for each entry in characters:
entry.style.render(entry.char, entry.x, entry.y)
// Usage:
// 1,000,000 characters but only ~10 unique style flyweights in pool
// Memory: 10 flyweight objects + lightweight position data per character
Example 2: Game Forest with Millions of Trees
// Flyweight - shared tree type data (heavy: textures, mesh)
class TreeType:
private name: String
private color: Color
private texture: Bitmap // Large! ~2MB per texture
private meshData: Mesh // Large! ~500KB per mesh
constructor(name, color, texturePath, meshPath):
this.name = name
this.color = color
this.texture = loadBitmap(texturePath)
this.meshData = loadMesh(meshPath)
method draw(x: int, y: int, age: float):
// Use shared texture/mesh, apply at extrinsic position and age-scale
canvas.drawBitmap(this.texture, x, y, scale=age)
// Flyweight Factory
class TreeTypeFactory:
private static types: Map<String, TreeType> = {}
static method getTreeType(name, color, texturePath, meshPath) -> TreeType:
key = name + "_" + color.toString()
if key NOT in types:
types[key] = new TreeType(name, color, texturePath, meshPath)
return types[key]
// Context object - stores extrinsic state
class Tree:
private x: int
private y: int
private age: float
private type: TreeType // Reference to shared flyweight
constructor(x, y, age, type):
this.x = x; this.y = y; this.age = age; this.type = type
method draw():
type.draw(this.x, this.y, this.age)
// Client
class Forest:
private trees: List<Tree> = []
method plantTree(x, y, age, name, color, texturePath, meshPath):
type = TreeTypeFactory.getTreeType(name, color, texturePath, meshPath)
trees.add(new Tree(x, y, age, type))
method render():
for each tree in trees:
tree.draw()
// Result: 1,000,000 trees but only ~5 TreeType flyweights
// Without flyweight: 1M × 2.5MB = 2.5 TB (impossible)
// With flyweight: 5 × 2.5MB + 1M × 20 bytes = 12.5MB + 20MB = 32.5MB
Intrinsic vs Extrinsic State
| Aspect | Intrinsic State | Extrinsic State |
|---|---|---|
| Definition | State independent of context | State dependent on context |
| Storage | Inside the flyweight object | Outside, in client or separate structure |
| Sharing | Shared across all usages | Unique per usage/context |
| Mutability | Immutable (must be) | Can be mutable |
| Lifecycle | Lives as long as flyweight exists | Computed/passed per operation |
| Memory impact | Stored once, saves memory | Stored per instance, but lightweight |
| Text editor example | Font, size, color, weight | Character value, x/y position |
| Game tree example | Texture, mesh, species name | Position (x,y), age, health |
| Web icon example | SVG path data, default color | Size override, tooltip, click handler |
| DB pool example | Driver class, connection URL | Transaction state, current query |
| Identification | "What kind of thing is this?" | "Where/how is this thing used right now?" |
Real-World Examples
1. Java String Interning
String s1 = "hello"; // Goes to string pool
String s2 = "hello"; // Returns same reference from pool
assert s1 == s2; // true - same object!
String s3 = new String("hello").intern(); // Explicit interning
The JVM maintains a string pool (flyweight factory). Identical string literals share one object.
2. Java Integer Cache (-128 to 127)
Integer a = Integer.valueOf(100); // Cached flyweight
Integer b = Integer.valueOf(100); // Same object returned
assert a == b; // true
Integer c = Integer.valueOf(200); // Outside cache range
Integer d = Integer.valueOf(200); // New object
assert c == d; // false
Integer.valueOf() acts as a flyweight factory for the range [-128, 127].
3. Game Sprite/Texture Sharing
Game engines like Unity share sprite atlases and materials across thousands of game objects. The SpriteRenderer references a shared Sprite asset (flyweight) while each GameObject has its own transform (extrinsic).
4. Icon Libraries (Font Awesome, Material Icons)
Icon fonts store glyph data once. Each usage on a page references the shared glyph with different size, color, and position (extrinsic state applied via CSS).
5. Browser DOM and CSS
CSS classes are flyweights - style rules defined once, applied to thousands of elements. Each element's position and content is extrinsic; the styling is shared intrinsic state.
Flyweight vs Singleton vs Object Pool
| Aspect | Flyweight | Singleton | Object Pool |
|---|---|---|---|
| Intent | Share fine-grained objects to save memory | Ensure exactly one instance globally | Reuse expensive objects to avoid creation cost |
| Number of instances | Multiple (one per unique intrinsic state) | Exactly one | Fixed or bounded set |
| Sharing model | Concurrent sharing (many clients, same time) | Global access point | Exclusive borrowing (one client at a time) |
| Object mutability | Immutable (intrinsic state) | Typically mutable | Mutable (reset between uses) |
| Client awareness | Client manages extrinsic state | Client just calls the instance | Client borrows and returns |
| Creation control | Factory creates/caches | Private constructor + static access | Pool manages lifecycle |
| Identity matters? | No (interchangeable) | Yes (the one instance) | Yes (tracked for return) |
| Example | Character styles in editor | Logger, Config manager | DB connections, thread pool |
| Memory optimization | Primary goal | Not a goal | Secondary benefit |
| Thread safety concern | Safe (immutable) | Must synchronize mutable state | Must synchronize borrow/return |
Advantages & Disadvantages
| Advantages | Disadvantages |
|---|---|
| Dramatically reduces memory usage when many similar objects exist | Increases code complexity - state split across flyweight and client |
| Reduces object creation time (reuse from pool) | CPU trade-off: extrinsic state must be recomputed or passed each time |
| Improves cache locality (fewer distinct objects) | Makes debugging harder - shared state can be confusing |
| Reduces GC pressure in managed languages | Clients become responsible for managing extrinsic state |
| Centralizes shared state management | Not beneficial when objects have mostly unique state |
| Thread-safe by design (immutable shared state) | Flyweight factory can become a bottleneck if not designed carefully |
| Scales well - adding more contexts doesn't add flyweights | Violates intuition that each object "owns" all its state |
Constraints & Edge Cases
Thread Safety of Shared Flyweights
Since flyweights are shared concurrently, they must be immutable:
- All intrinsic fields should be
final/readonly - No setters on intrinsic state
- If the flyweight factory uses lazy initialization, the factory's
getFlyweight()method needs synchronization (or useConcurrentHashMap.computeIfAbsent()) - The extrinsic state is per-client, so no sharing conflicts there
// Thread-safe factory in Java
class FlyweightFactory:
private pool: ConcurrentHashMap<String, Flyweight> = new ConcurrentHashMap<>()
method getFlyweight(key) -> Flyweight:
return pool.computeIfAbsent(key, k -> new ConcreteFlyweight(k))
Memory vs CPU Trade-off
Flyweight saves memory but may cost CPU:
- Extrinsic state must be stored somewhere (arrays, parallel structures) or recomputed each time
- Method calls require passing extra parameters (extrinsic state)
- If extrinsic state computation is expensive, the pattern may hurt performance
- Profile first: Only apply when memory is the proven bottleneck
When Sharing Isn't Worth the Complexity
Avoid Flyweight when:
- Object count is small (< 1000) - overhead of factory exceeds savings
- Intrinsic state is tiny relative to extrinsic state - minimal sharing benefit
- Objects are short-lived - GC handles them efficiently anyway
- The state separation makes the code significantly harder to maintain
- Object identity semantics are required (e.g., observer patterns where each listener must be distinct)
Edge Cases
- Flyweight eviction: In memory-constrained environments, consider weak references in the pool so unused flyweights can be GC'd
- Serialization: Shared flyweights complicate serialization - deserializing must re-establish sharing via the factory
- Equality semantics: Since flyweights are shared,
==(reference equality) works for comparing intrinsic state, but clients must not rely on this for objects with different extrinsic state
Structural Patterns Summary Comparison
Since this is the final article in the Structural Patterns section, here is a comprehensive comparison of all seven structural patterns:
| Aspect | Adapter | Bridge | Composite | Decorator | Facade | Flyweight | Proxy |
|---|---|---|---|---|---|---|---|
| Intent | Convert interface to one client expects | Separate abstraction from implementation | Treat individual and composite objects uniformly | Add responsibilities dynamically | Provide unified interface to subsystem | Share objects to support large quantities efficiently | Control access to an object |
| Key Mechanism | Wraps adaptee with compatible interface | Composition over inheritance; two hierarchies | Tree structure with uniform interface | Wrapping with same interface, adding behavior | Single entry point delegating to subsystem | Separate intrinsic (shared) from extrinsic (contextual) state | Surrogate that controls access to real subject |
| Participants | Target, Adapter, Adaptee, Client | Abstraction, Implementor, RefinedAbstraction, ConcreteImplementor | Component, Leaf, Composite, Client | Component, ConcreteComponent, Decorator, ConcreteDecorator | Facade, Subsystem classes, Client | Flyweight, ConcreteFlyweight, FlyweightFactory, Client | Subject, RealSubject, Proxy, Client |
| Primary Use Case | Integrate legacy/third-party code with incompatible interface | Avoid cartesian product of abstractions × implementations | File systems, UI component trees, org charts | Streams, middleware chains, UI enhancement | API gateways, library wrappers, service layers | Text rendering, game worlds, caches | Lazy loading, access control, remote objects, caching |
| What Changes | Interface compatibility | Implementation can vary independently | Structure (add/remove children) | Behavior (add/remove at runtime) | Simplification of complex API | Memory footprint | Access semantics |
| Transparency | Client sees Target interface | Client sees Abstraction | Client sees Component uniformly | Client sees Component interface | Client sees simplified interface | Client manages extrinsic state (not fully transparent) | Client sees Subject interface |
| Related Patterns | Bridge, Decorator, Proxy | Adapter, Strategy, Abstract Factory | Iterator, Visitor, Decorator | Composite, Strategy, Proxy | Mediator, Singleton, Abstract Factory | Composite (often flyweight leaves), Factory | Adapter, Decorator, Facade |
Quick Decision Guide
Need to... → Use
─────────────────────────────────────────────────────────
Make incompatible interfaces work → Adapter
Decouple abstraction from implementation → Bridge
Represent part-whole hierarchies → Composite
Add behavior without subclassing → Decorator
Simplify a complex subsystem → Facade
Reduce memory for many similar objects → Flyweight
Control access or add indirection → Proxy
Interview Follow-ups
Q1: How does Flyweight differ from caching?
A: Caching stores computed results to avoid re-computation (temporal optimization). Flyweight shares object structure to avoid duplication (spatial optimization). A cache entry is typically a complete result; a flyweight is an incomplete object that requires extrinsic state to function. Caches have eviction policies; flyweight pools typically retain objects indefinitely since they're actively shared.
Q2: Can a Flyweight have mutable intrinsic state?
A: No. If intrinsic state were mutable, changing it for one client would affect all clients sharing that flyweight - violating correctness. Immutability is a hard requirement for the shared intrinsic state. If you need mutable shared state, you need synchronization and are likely looking at a different pattern (e.g., shared mutable state with locks, or the Prototype pattern for copy-on-write).
Q3: How would you implement Flyweight in a distributed system?
A: In distributed systems, flyweight manifests differently. You can't share in-memory objects across processes, but you can: (1) Use a shared cache (Redis) as the flyweight pool, with each service fetching shared immutable data by key. (2) Use content-addressable storage where identical data maps to the same hash/key. (3) Apply deduplication at the storage layer. The principle remains - identify shared immutable state and store it once with references from many contexts.
Q4 (Hint Only): "Design a system that renders a map with 10 million building objects. How would you optimize memory?"
Hint: Think about what's shared across buildings of the same type (texture, model, material) vs. what's unique (GPS coordinates, floor count, age). Consider a BuildingType flyweight factory. How would you handle LOD (level of detail) - is that intrinsic or extrinsic?
Q5 (Hint Only): "Java's String.intern() can cause memory leaks in older JVMs. Why, and how does this relate to Flyweight?"
Hint: Consider where interned strings were stored (PermGen in pre-Java 7) and what happens when the flyweight pool grows unboundedly. Think about eviction policies and weak references. How would you design a flyweight pool that avoids this problem?
Counter Questions to Ask Interviewer
-
"What's the expected object count and how much memory is available?" - Flyweight only makes sense at scale. If we're dealing with hundreds of objects, simpler approaches suffice.
-
"How much of the object state is truly shared vs. unique?" - If 90% of state is unique, flyweight provides minimal benefit. Need to quantify the intrinsic/extrinsic ratio.
-
"Are there concurrency requirements for the flyweight pool?" - Determines whether we need
ConcurrentHashMap, double-checked locking, or can use a simpleHashMapin single-threaded contexts. -
"Is object identity important in this system?" - If clients use reference equality or need distinct observer registrations per object, flyweight's sharing model breaks assumptions.
-
"What's the acceptable latency trade-off?" - Flyweight saves memory but adds indirection. If this is a hot path with nanosecond sensitivity, the extra method parameter passing and factory lookup may matter.
References & Whitepapers
-
Gamma, E., Helm, R., Johnson, R., Vlissides, J. - Design Patterns: Elements of Reusable Object-Oriented Software (1994), Chapter 4: Structural Patterns - Flyweight. The canonical reference defining intrinsic/extrinsic state separation.
-
Java Language Specification - §3.10.5 String Literals: Documents string interning behavior and the string constant pool. JLS §3.10.5
-
Java
IntegerCache -Integer.valueOf()implementation caching [-128, 127]. Source:java.lang.Integer(OpenJDK). -
Nystrom, Robert - Game Programming Patterns (2014), Chapter "Flyweight". Excellent treatment of the pattern in game development with texture/sprite sharing examples. gameprogrammingpatterns.com/flyweight.html
-
Bloch, Joshua - Effective Java (3rd Edition), Item 6: "Avoid creating unnecessary objects" - discusses flyweight-adjacent techniques including static factory methods and instance caching.
-
Oracle Documentation - String.intern() method specification and performance characteristics in modern JVMs (G1 string deduplication).
Related Topics
- Adapter Pattern - Structural pattern for interface conversion
- Bridge Pattern - Decoupling abstraction from implementation
- Composite Pattern - Often uses Flyweight for leaf nodes
- Decorator Pattern - Adds behavior; Flyweight reduces memory
- Facade Pattern - Simplifies subsystem access
- Proxy Pattern - Controls access; can cache flyweights
- Factory Method - Flyweight Factory is a specialized factory
- Singleton - Single instance vs. shared pool of instances
- Object Pool - Related reuse pattern with exclusive borrowing
- Prototype - Clone vs. share trade-off