Flyweight Pattern: Memory Optimization Design
TL;DR
The Flyweight pattern reduces memory usage by sharing common state (intrinsic state) among multiple objects, while keeping unique state (extrinsic state) external. This structural pattern is essential when you need to create thousands or millions of similar objects efficiently.
Core Concept
What is the Flyweight Pattern?
The Flyweight pattern is a structural design pattern that minimizes memory usage by sharing as much data as possible with similar objects. Instead of storing all data in every object instance, flyweights separate state into two categories:
- Intrinsic state: Shared, immutable data stored inside the flyweight object (e.g., character font, tree species)
- Extrinsic state: Unique, context-dependent data passed to the flyweight from outside (e.g., character position, tree coordinates)
Why It Matters
When your application needs to create thousands or millions of objects with mostly similar data, storing everything independently wastes memory. A text editor with 100,000 characters doesn’t need 100,000 separate font objects — it can share font objects and only store position/content separately.
Memory savings can be dramatic: instead of 1 million objects each holding 1KB of shared data (1GB total), you might have 10 flyweight objects (10KB) plus 1 million lightweight references.
Core Components
Flyweight Factory: Manages flyweight instances, ensuring shared objects are reused rather than recreated. Acts as a cache.
Flyweight: Stores intrinsic state and accepts extrinsic state as method parameters.
Client: Maintains or computes extrinsic state and passes it to flyweight methods.
When to Use
- Your application uses a large number of objects
- Storage costs are high due to object quantity
- Most object state can be made extrinsic
- Many groups of objects can be replaced by relatively few shared objects
- The application doesn’t depend on object identity (using
==instead ofis)
Common use cases include text rendering systems, game development (particles, tiles), and data visualization with repeated graphical elements.
Visual Guide
Flyweight Pattern Structure
classDiagram
class FlyweightFactory {
-flyweights: dict
+get_flyweight(key) Flyweight
}
class Flyweight {
-intrinsic_state
+operation(extrinsic_state)
}
class Client {
-extrinsic_state
-flyweight: Flyweight
+operation()
}
FlyweightFactory --> Flyweight: creates/returns
Client --> Flyweight: uses
Client --> FlyweightFactory: requests flyweight
note for Flyweight "Stores shared,\nimmutable state"
note for Client "Stores unique,\ncontext-specific state"
The factory ensures flyweights are shared. Clients hold extrinsic state and pass it to flyweight methods.
Memory Comparison: With vs Without Flyweight
graph LR
subgraph Without Flyweight
A1[Object 1<br/>All State]
A2[Object 2<br/>All State]
A3[Object 3<br/>All State]
A4[Object ...<br/>All State]
A5[Object 1000<br/>All State]
end
subgraph With Flyweight
B1[Flyweight A<br/>Shared State]
B2[Flyweight B<br/>Shared State]
C1[Context 1<br/>Unique State] --> B1
C2[Context 2<br/>Unique State] --> B1
C3[Context 3<br/>Unique State] --> B2
C4[Context ...<br/>Unique State] --> B2
C5[Context 1000<br/>Unique State] --> B1
end
Without flyweight: N objects with full state. With flyweight: Few shared flyweights + N lightweight contexts.
Examples
Example 1: Text Editor Character Rendering
Imagine a text editor displaying 100,000 characters. Without flyweight, each character object stores font, size, and style — wasting memory on repeated data.
class CharacterFlyweight:
"""Flyweight storing intrinsic state (shared formatting)"""
def __init__(self, font, size, style):
self.font = font # Intrinsic: shared
self.size = size # Intrinsic: shared
self.style = style # Intrinsic: shared
def render(self, char, position):
"""Extrinsic state (char, position) passed as parameters"""
return f"Rendering '{char}' at {position} with {self.font} {self.size}pt {self.style}"
class CharacterFactory:
"""Factory ensures flyweights are shared"""
def __init__(self):
self._flyweights = {}
def get_character_format(self, font, size, style):
key = (font, size, style)
if key not in self._flyweights:
self._flyweights[key] = CharacterFlyweight(font, size, style)
print(f"Creating new flyweight for {key}")
return self._flyweights[key]
def get_flyweight_count(self):
return len(self._flyweights)
class Character:
"""Client holding extrinsic state"""
def __init__(self, char, position, flyweight):
self.char = char # Extrinsic: unique per character
self.position = position # Extrinsic: unique per character
self._flyweight = flyweight
def display(self):
return self._flyweight.render(self.char, self.position)
# Usage
factory = CharacterFactory()
# Create characters - most share formatting
characters = []
characters.append(Character('H', (0, 0), factory.get_character_format('Arial', 12, 'bold')))
characters.append(Character('e', (10, 0), factory.get_character_format('Arial', 12, 'bold')))
characters.append(Character('l', (20, 0), factory.get_character_format('Arial', 12, 'bold')))
characters.append(Character('l', (30, 0), factory.get_character_format('Arial', 12, 'bold')))
characters.append(Character('o', (40, 0), factory.get_character_format('Arial', 12, 'bold')))
characters.append(Character('!', (50, 0), factory.get_character_format('Arial', 16, 'italic')))
for char in characters:
print(char.display())
print(f"\nTotal flyweights created: {factory.get_flyweight_count()}")
print(f"Total characters: {len(characters)}")
Expected Output:
Creating new flyweight for ('Arial', 12, 'bold')
Rendering 'H' at (0, 0) with Arial 12pt bold
Rendering 'e' at (10, 0) with Arial 12pt bold
Rendering 'l' at (20, 0) with Arial 12pt bold
Rendering 'l' at (30, 0) with Arial 12pt bold
Rendering 'o' at (40, 0) with Arial 12pt bold
Creating new flyweight for ('Arial', 16, 'italic')
Rendering '!' at (50, 0) with Arial 16pt italic
Total flyweights created: 2
Total characters: 6
Try it yourself: Add 1000 more characters with the same two formats. Notice the flyweight count stays at 2.
Example 2: Forest Simulation with Trees
A game rendering 100,000 trees in a forest. Each tree has a type (oak, pine, birch) with shared texture/mesh data, but unique position and age.
from dataclasses import dataclass
from typing import Tuple
@dataclass(frozen=True) # Immutable flyweight
class TreeType:
"""Flyweight: shared tree characteristics"""
name: str
texture: str
mesh_data: str # Simplified - would be actual 3D model
def render(self, x: int, y: int, age: int):
return f"Drawing {self.name} tree at ({x}, {y}), age {age} years"
class TreeFactory:
"""Manages tree type flyweights"""
_tree_types = {}
@classmethod
def get_tree_type(cls, name: str, texture: str, mesh_data: str) -> TreeType:
key = (name, texture, mesh_data)
if key not in cls._tree_types:
cls._tree_types[key] = TreeType(name, texture, mesh_data)
return cls._tree_types[key]
@classmethod
def get_type_count(cls):
return len(cls._tree_types)
class Tree:
"""Client: holds extrinsic state"""
def __init__(self, x: int, y: int, age: int, tree_type: TreeType):
self.x = x # Extrinsic
self.y = y # Extrinsic
self.age = age # Extrinsic
self._type = tree_type # Reference to flyweight
def draw(self):
return self._type.render(self.x, self.y, self.age)
class Forest:
"""Manages all trees"""
def __init__(self):
self.trees = []
def plant_tree(self, x: int, y: int, age: int,
name: str, texture: str, mesh: str):
tree_type = TreeFactory.get_tree_type(name, texture, mesh)
tree = Tree(x, y, age, tree_type)
self.trees.append(tree)
def draw_forest(self):
for tree in self.trees:
print(tree.draw())
# Usage
forest = Forest()
# Plant many trees - only 3 types
forest.plant_tree(10, 20, 5, "Oak", "oak_texture.png", "oak_mesh")
forest.plant_tree(30, 40, 3, "Pine", "pine_texture.png", "pine_mesh")
forest.plant_tree(50, 60, 7, "Oak", "oak_texture.png", "oak_mesh")
forest.plant_tree(70, 80, 2, "Birch", "birch_texture.png", "birch_mesh")
forest.plant_tree(90, 100, 4, "Pine", "pine_texture.png", "pine_mesh")
forest.plant_tree(110, 120, 6, "Oak", "oak_texture.png", "oak_mesh")
forest.draw_forest()
print(f"\nTotal trees: {len(forest.trees)}")
print(f"Unique tree types (flyweights): {TreeFactory.get_type_count()}")
Expected Output:
Drawing Oak tree at (10, 20), age 5 years
Drawing Pine tree at (30, 40), age 3 years
Drawing Oak tree at (50, 60), age 7 years
Drawing Birch tree at (70, 80), age 2 years
Drawing Pine tree at (90, 100), age 4 years
Drawing Oak tree at (110, 120), age 6 years
Total trees: 6
Unique tree types (flyweights): 3
Java/C++ Note: In Java, use HashMap for the factory cache. In C++, use std::unordered_map with a custom hash function for composite keys, or use std::map with std::tuple keys.
Try it yourself: Simulate planting 10,000 trees with random positions but only 5 tree types. Calculate memory savings compared to storing all data in each tree object.
Common Mistakes
1. Storing Extrinsic State in Flyweights
Wrong:
class CharacterFlyweight:
def __init__(self, font, size, char, position): # char and position are extrinsic!
self.font = font
self.size = size
self.char = char # WRONG: unique per instance
self.position = position # WRONG: unique per instance
This defeats the purpose — you can’t share flyweights if they contain unique data. Intrinsic state must be immutable and shareable. Pass extrinsic state as method parameters.
2. Not Using a Factory
Wrong:
# Client creates flyweights directly
char1 = CharacterFlyweight('Arial', 12, 'bold')
char2 = CharacterFlyweight('Arial', 12, 'bold') # Duplicate!
Without a factory, you create duplicate flyweights instead of reusing them. The factory’s cache is essential for the pattern to work. Always access flyweights through a factory that checks for existing instances.
3. Mutating Flyweight State
Wrong:
flyweight = factory.get_flyweight('Arial', 12)
flyweight.size = 14 # WRONG: modifies shared object!
If you modify a flyweight, you affect all clients using it. Flyweights must be immutable after creation. Use Python’s @dataclass(frozen=True) or make attributes private with no setters.
4. Applying Flyweight When Object Count is Low
Using flyweight for 10-100 objects adds complexity without meaningful benefit. The pattern’s overhead (factory, indirection) only pays off with thousands of objects. Profile first — premature optimization wastes development time.
5. Confusing Flyweight with Singleton
Singleton ensures one instance of a class exists. Flyweight allows multiple shared instances based on intrinsic state. You might have 50 flyweights (different font/size combinations) but thousands of clients. They solve different problems — don’t conflate them.
Interview Tips
What Interviewers Look For
1. Clear Intrinsic vs Extrinsic Distinction
When asked to implement flyweight, immediately identify what’s shared (intrinsic) vs unique (extrinsic). Say: “Font and size are intrinsic — shared across characters. Position and the character itself are extrinsic — unique per instance.” This shows you understand the core concept.
2. Factory Implementation
Interviewers expect you to implement the factory cache correctly. Use a dictionary with composite keys (tuples). Explain: “The factory maintains a cache keyed by intrinsic state. Before creating a flyweight, we check if one exists for these parameters.”
3. Memory Analysis
Be ready to calculate memory savings. Example: “Without flyweight, 100,000 characters × 1KB per character = 100MB. With flyweight, 10 formats × 1KB + 100,000 lightweight references × 16 bytes = 10KB + 1.6MB ≈ 1.6MB total. That’s 98% reduction.”
4. When NOT to Use Flyweight
Interviewers test judgment. Say: “Flyweight adds complexity through indirection and factory management. I’d only use it when profiling shows memory issues with large object counts — typically thousands or more. For small datasets, the overhead isn’t worth it.”
5. Common Follow-up: Thread Safety
If asked about concurrency, mention: “The factory cache needs synchronization in multi-threaded environments. In Python, use threading.Lock. In Java, use ConcurrentHashMap or synchronize the factory method.”
6. Real-World Examples
Mention concrete use cases: text editors (character formatting), game engines (particles, tiles), GUI frameworks (widget styles), string interning in Java/Python. This demonstrates practical understanding beyond theory.
7. Code Walkthrough Strategy
When implementing on a whiteboard:
- Define the flyweight class (intrinsic state only)
- Create the factory with cache
- Show client code holding extrinsic state
- Demonstrate how extrinsic state is passed to flyweight methods
This structured approach shows clear thinking under pressure.
Key Takeaways
- Flyweight separates state: intrinsic (shared, immutable) stored in flyweights; extrinsic (unique) passed as parameters or stored in clients
- Factory is essential: manages flyweight cache to ensure sharing; without it, you create duplicates and waste memory
- Use when object count is high: pattern overhead only justified with thousands+ of similar objects; profile before optimizing
- Flyweights must be immutable: modifying shared state affects all clients; use frozen dataclasses or private attributes
- Memory savings can be dramatic: reduces storage from O(n) full objects to O(k) flyweights + O(n) lightweight contexts, where k << n