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 UseWhen NOT to Use
Application uses a large number of similar objectsFew objects exist in the system
Storage costs are high due to sheer object quantityObjects have mostly unique state
Most object state can be made extrinsicExtrinsic state cannot be easily computed or passed
Groups of objects can be replaced by fewer shared objects + extrinsic stateObject 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 bottleneckComplexity 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:

  1. Check if a matching flyweight already exists in the pool
  2. If yes → return the existing shared instance
  3. 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

AspectIntrinsic StateExtrinsic State
DefinitionState independent of contextState dependent on context
StorageInside the flyweight objectOutside, in client or separate structure
SharingShared across all usagesUnique per usage/context
MutabilityImmutable (must be)Can be mutable
LifecycleLives as long as flyweight existsComputed/passed per operation
Memory impactStored once, saves memoryStored per instance, but lightweight
Text editor exampleFont, size, color, weightCharacter value, x/y position
Game tree exampleTexture, mesh, species namePosition (x,y), age, health
Web icon exampleSVG path data, default colorSize override, tooltip, click handler
DB pool exampleDriver class, connection URLTransaction 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

AspectFlyweightSingletonObject Pool
IntentShare fine-grained objects to save memoryEnsure exactly one instance globallyReuse expensive objects to avoid creation cost
Number of instancesMultiple (one per unique intrinsic state)Exactly oneFixed or bounded set
Sharing modelConcurrent sharing (many clients, same time)Global access pointExclusive borrowing (one client at a time)
Object mutabilityImmutable (intrinsic state)Typically mutableMutable (reset between uses)
Client awarenessClient manages extrinsic stateClient just calls the instanceClient borrows and returns
Creation controlFactory creates/cachesPrivate constructor + static accessPool manages lifecycle
Identity matters?No (interchangeable)Yes (the one instance)Yes (tracked for return)
ExampleCharacter styles in editorLogger, Config managerDB connections, thread pool
Memory optimizationPrimary goalNot a goalSecondary benefit
Thread safety concernSafe (immutable)Must synchronize mutable stateMust synchronize borrow/return

Advantages & Disadvantages

AdvantagesDisadvantages
Dramatically reduces memory usage when many similar objects existIncreases 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 languagesClients become responsible for managing extrinsic state
Centralizes shared state managementNot 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 flyweightsViolates 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 use ConcurrentHashMap.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:

AspectAdapterBridgeCompositeDecoratorFacadeFlyweightProxy
IntentConvert interface to one client expectsSeparate abstraction from implementationTreat individual and composite objects uniformlyAdd responsibilities dynamicallyProvide unified interface to subsystemShare objects to support large quantities efficientlyControl access to an object
Key MechanismWraps adaptee with compatible interfaceComposition over inheritance; two hierarchiesTree structure with uniform interfaceWrapping with same interface, adding behaviorSingle entry point delegating to subsystemSeparate intrinsic (shared) from extrinsic (contextual) stateSurrogate that controls access to real subject
ParticipantsTarget, Adapter, Adaptee, ClientAbstraction, Implementor, RefinedAbstraction, ConcreteImplementorComponent, Leaf, Composite, ClientComponent, ConcreteComponent, Decorator, ConcreteDecoratorFacade, Subsystem classes, ClientFlyweight, ConcreteFlyweight, FlyweightFactory, ClientSubject, RealSubject, Proxy, Client
Primary Use CaseIntegrate legacy/third-party code with incompatible interfaceAvoid cartesian product of abstractions × implementationsFile systems, UI component trees, org chartsStreams, middleware chains, UI enhancementAPI gateways, library wrappers, service layersText rendering, game worlds, cachesLazy loading, access control, remote objects, caching
What ChangesInterface compatibilityImplementation can vary independentlyStructure (add/remove children)Behavior (add/remove at runtime)Simplification of complex APIMemory footprintAccess semantics
TransparencyClient sees Target interfaceClient sees AbstractionClient sees Component uniformlyClient sees Component interfaceClient sees simplified interfaceClient manages extrinsic state (not fully transparent)Client sees Subject interface
Related PatternsBridge, Decorator, ProxyAdapter, Strategy, Abstract FactoryIterator, Visitor, DecoratorComposite, Strategy, ProxyMediator, Singleton, Abstract FactoryComposite (often flyweight leaves), FactoryAdapter, 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

  1. "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.

  2. "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.

  3. "Are there concurrency requirements for the flyweight pool?" - Determines whether we need ConcurrentHashMap, double-checked locking, or can use a simple HashMap in single-threaded contexts.

  4. "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.

  5. "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

  1. 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.

  2. Java Language Specification - §3.10.5 String Literals: Documents string interning behavior and the string constant pool. JLS §3.10.5

  3. Java Integer Cache - Integer.valueOf() implementation caching [-128, 127]. Source: java.lang.Integer (OpenJDK).

  4. 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

  5. Bloch, Joshua - Effective Java (3rd Edition), Item 6: "Avoid creating unnecessary objects" - discusses flyweight-adjacent techniques including static factory methods and instance caching.

  6. Oracle Documentation - String.intern() method specification and performance characteristics in modern JVMs (G1 string deduplication).