Skip to main content

Flyweight Pattern

Share fine-grained objects efficiently—optimize memory for millions of similar instances

TL;DR

Flyweight optimizes memory by sharing common state among many objects. For a text editor with a million characters, create one Flyweight for each glyph (Arial-A, Arial-B, etc.) and reuse it, storing only the position/color separately. Separate intrinsic state (shared, immutable) from extrinsic state (per-instance). Use Flyweight only when profiling shows memory is actually a problem.

Learning Objectives

  • You will be able to identify opportunities for Flyweight optimization.
  • You will be able to separate intrinsic from extrinsic state correctly.
  • You will be able to implement a Flyweight factory for managing shared objects.
  • You will be able to recognize when Flyweight's complexity is not worth the savings.

Motivating Scenario

A game renders 100,000 trees on a landscape. Each tree object stores texture, 3D model, animation data (intrinsic—same for all Pine trees). Without Flyweight, that's 100,000 × 2MB = 200GB. With Flyweight: one Pine Flyweight (2MB) + 100,000 position/color entries (intrinsic stored once, extrinsic per instance) = ~5MB.

Core Concepts

Flyweight optimizes memory by sharing intrinsic state (unchanging, immutable) among many instances and externalizing extrinsic state (changeable, per-instance).

Key elements:

  • Intrinsic state: shared across all Flyweights (font, shape, immutable data)
  • Extrinsic state: stored outside, passed to Flyweight methods (position, color, size)
  • FlyweightFactory: creates and caches Flyweights, ensuring reuse
  • Client: stores extrinsic state and passes it to Flyweights
Flyweight structure

Practical Example

# Flyweight: intrinsic state (shared, immutable)
class CharacterGlyph:
def __init__(self, char: str, font_name: str, font_size: int):
self.char = char
self.font_name = font_name
self.font_size = font_size

def render(self, x: int, y: int, color: str) -> str:
# Extrinsic state: x, y, color passed in
return f"Render '{self.char}' at ({x},{y}) in {self.font_name}:{self.font_size}, color={color}"

# FlyweightFactory: manage and reuse Flyweights
class CharacterGlyphFactory:
def __init__(self):
self._glyphs = {}

def get_glyph(self, char: str, font_name: str, font_size: int) -> CharacterGlyph:
key = (char, font_name, font_size)
if key not in self._glyphs:
self._glyphs[key] = CharacterGlyph(char, font_name, font_size)
return self._glyphs[key]

def get_cache_size(self) -> int:
return len(self._glyphs)

# Client: stores extrinsic state separately
class TextDocument:
def __init__(self):
self.glyph_factory = CharacterGlyphFactory()
self.characters = [] # List of (glyph, x, y, color)

def add_character(self, char: str, font_name: str, font_size: int, x: int, y: int, color: str):
glyph = self.glyph_factory.get_glyph(char, font_name, font_size)
self.characters.append((glyph, x, y, color))

def render_all(self) -> str:
return "\n".join(
glyph.render(x, y, color)
for glyph, x, y, color in self.characters
)

def get_stats(self) -> str:
return f"Characters in document: {len(self.characters)}, Unique glyphs cached: {self.glyph_factory.get_cache_size()}"

# Usage: memory-efficient for large documents
doc = TextDocument()
doc.add_character("H", "Arial", 12, 10, 20, "black")
doc.add_character("e", "Arial", 12, 15, 20, "black")
doc.add_character("l", "Arial", 12, 20, 20, "black")
doc.add_character("l", "Arial", 12, 25, 20, "black") # Reuses 'l' Flyweight
doc.add_character("o", "Arial", 12, 30, 20, "black")

print(doc.render_all())
print(doc.get_stats()) # "Characters in document: 5, Unique glyphs cached: 4"

When to Use / When NOT to Use

Use Flyweight when:
  1. You have many similar objects (thousands or millions)
  2. Memory is a proven bottleneck (profiling confirms this)
  3. Objects share substantial intrinsic (immutable) state
  4. Extrinsic state can be separated and stored elsewhere
  5. Creating and destroying objects frequently is expensive
Don't use Flyweight when:
  1. Memory usage is not a problem (premature optimization)
  2. Objects don't share enough state to justify the complexity
  3. Intrinsic state is mutable or hard to identify
  4. The overhead of the factory and lookup outweighs savings
  5. Simplicity and readability are more important than memory

Patterns and Pitfalls

Patterns and Pitfalls

Only optimize after profiling proves memory is a bottleneck.
Make the distinction explicit in code.
Use locks if multiple threads access the factory.

Design Review Checklist

  • Profiling confirms memory is actually a bottleneck
  • Intrinsic state is clearly identified and truly immutable
  • Extrinsic state is cleanly separated and passed in
  • FlyweightFactory manages creation and caching correctly
  • Clients store extrinsic state, not Flyweights
  • Thread safety is handled if concurrent access occurs
  • Cache has reasonable bounds (no unbounded memory growth)
  • Performance gain (memory savings) outweighs implementation complexity

Self-Check

  1. Profile: Measure memory usage of your current solution.
  2. Analyze: Identify intrinsic (shared) vs. extrinsic (per-instance) state.
  3. Implement: Build a Flyweight with Factory and verify memory savings.
info

One Takeaway: Flyweight optimizes memory by sharing intrinsic (immutable) state across many instances and externalizing extrinsic (changeable) state. Only use Flyweight when profiling proves memory is a bottleneck—the complexity is not worth it otherwise. Separate intrinsic from extrinsic state clearly, use a factory for reuse, and ensure the Flyweight is truly immutable.

Next Steps

  • Learn Object Pool for reusing expensive objects (similar optimization pattern).
  • Study Factory patterns for efficient object creation.
  • Explore Memento for saving state without memory overhead.

References

  • Gang of Four: Design Patterns (Flyweight)
  • Head First Design Patterns (Flyweight chapter)
  • Microsoft Docs: Flyweight Pattern