Structural

Flyweight

Use sharing to efficiently support a large number of fine-grained objects by externalising state.

Advanced Pattern #11 of 23

What is the Flyweight Pattern?

Flyweight minimises memory usage by sharing as much data as possible among similar objects. It separates object state into intrinsic (shared, immutable, stored in the flyweight) and extrinsic (unique per context, passed in at runtime). One flyweight object serves many contexts.

The key insight: if many objects share the same internal state, there's no need to store that state in each object — store it once in a shared flyweight and pass the context-specific part in when needed.

Real-world analogy: A forest of trees in a game. Each tree has position (extrinsic — unique per tree) and species/texture/colour (intrinsic — shared by all trees of the same type). Instead of storing texture data in 10,000 tree objects, store it once in one TreeType flyweight used by all.

The Problem It Solves

A text editor stores each character as an object with font, size, colour, and the character value itself. A document with 100,000 characters creates 100,000 objects, most sharing the same font/size/colour — massive, redundant memory consumption.

Flyweight stores font, size, and colour once in a shared CharacterStyle object. Each character position only stores the character value and a reference to the shared style — dramatically reducing memory.

Participants

Flyweight (interface)
Declares the interface through which flyweights receive extrinsic state and perform operations.
ConcreteFlyweight
Stores intrinsic state — shared, immutable data. Must be thread-safe as it is shared. Accepts extrinsic state as method parameters.
FlyweightFactory
Creates and manages flyweight objects. Returns an existing flyweight if one with the requested intrinsic state already exists; creates a new one otherwise. The factory is essentially a cache.
Context (Client)
Stores the extrinsic state — unique per object. Calls flyweight operations passing in extrinsic state. Has a reference to a shared flyweight.

Visual Flow Diagram

FlyweightFactory cache: Map<key, Flyweight> getFlyweight(key) → shared returns existing or creates new ConcreteFlyweight intrinsic: color, texture (shared, immutable) draw(x, y) ← extrinsic Context (Tree) x=10, y=45 (extrinsic) → shared TreeType Context (Tree) x=200,y=88 (extrinsic) → shared TreeType creates/returns ← many contexts share one flyweight → Memory: 10,000 trees × 1 shared TreeType (not 10,000 TreeTypes) — saves megabytes

Java Code Example

Java — Forest / Game Tree Example
// Flyweight — stores INTRINSIC state (shared, immutable)
public class TreeType {
    private final String name;    // intrinsic
    private final String color;   // intrinsic
    private final String texture; // intrinsic (large data!)

    public TreeType(String name, String color, String texture) {
        this.name=name; this.color=color; this.texture=texture;
    }

    // Extrinsic state (x, y) passed in at call time — NOT stored here
    public void draw(int x, int y) {
        System.out.printf("Drawing %s tree [%s] at (%d,%d)%n", name, color, x, y);
    }
}

// FlyweightFactory — cache of shared TreeType instances
public class TreeFactory {
    private static final Map<String, TreeType> cache = new HashMap<>();

    public static TreeType getTreeType(String name, String color, String tex) {
        String key = name + "_" + color;
        return cache.computeIfAbsent(key, k -> {
            System.out.println("Creating new TreeType: " + key);
            return new TreeType(name, color, tex); // expensive creation once
        });
    }

    public static int getCacheSize() { return cache.size(); }
}

// Context — stores EXTRINSIC state (unique per tree instance)
public class Tree {
    private final int x, y;           // extrinsic — unique per tree
    private final TreeType type;     // reference to shared flyweight

    public Tree(int x, int y, TreeType type) {
        this.x=x; this.y=y; this.type=type;
    }

    public void draw() {
        type.draw(x, y); // passes extrinsic state to flyweight
    }
}

// Forest — creates 10,000 trees using only 2 TreeType objects
public class Forest {
    private final List<Tree> trees = new ArrayList<>();

    public void plantTree(int x, int y, String name, String color, String tex) {
        TreeType type = TreeFactory.getTreeType(name, color, tex);
        trees.add(new Tree(x, y, type)); // Tree is tiny — just x,y + reference
    }

    public void draw() { trees.forEach(Tree::draw); }
}

// Client
public class Main {
    public static void main(String[] args) {
        Forest forest = new Forest();

        for (int i = 0; i < 5000; i++)
            forest.plantTree(i*2, i*3, "Oak", "Green", "oak_texture");

        for (int i = 0; i < 5000; i++)
            forest.plantTree(i*4, i*5, "Pine", "DarkGreen", "pine_texture");

        System.out.println("TreeType objects in cache: " + TreeFactory.getCacheSize());
        // Output: TreeType objects in cache: 2  (not 10,000!)

        forest.draw(); // all 10,000 trees rendered
    }
}

When to Use / Avoid

✓ Use When

  • Application uses a huge number of similar objects that consume too much memory
  • Most object state can be made extrinsic (passed in, not stored)
  • Many groups of objects can be replaced by relatively few shared objects
  • Application doesn't depend on object identity — shared objects look identical

✕ Avoid When

  • Object count is low — the complexity isn't justified
  • State is mostly unique per object — little to share as intrinsic state
  • You need to distinguish objects by identity (==) — sharing breaks this

Real-World Examples

Pros & Cons

Pros

  • Dramatically reduces memory usage when many similar objects needed
  • Can improve performance — fewer objects = less GC pressure
  • Enables scenarios otherwise impossible due to memory constraints

Cons

  • Increases complexity — must carefully separate intrinsic vs extrinsic state
  • Extrinsic state must be passed around — more parameters in method signatures
  • Shared mutable flyweights cause race conditions — flyweights must be immutable
  • May trade RAM savings for CPU cost (computing extrinsic state each call)

How Flyweight Can Be Broken

⚠ Attack Vectors

  • Mutable intrinsic state: If a flyweight's shared state is mutable, one context changing it corrupts all contexts that share the same flyweight — the most dangerous Flyweight failure mode
  • Storing extrinsic state in the flyweight: If context-specific data (x, y, userId) is stored in the shared object, concurrent requests overwrite each other's data — thread-safety nightmare
  • Identity equality broken: Code using == to compare tree objects gets wrong results since different Tree contexts share the same TreeType — subtle equality bugs
  • Factory cache unbounded growth: If the factory key is too granular (e.g., includes extrinsic state), the cache grows without bound — defeating the memory-saving purpose and causing a memory leak
  • Thread-unsafe factory: Concurrent calls to getFlyweight() without synchronisation can create duplicate flyweight objects

✓ Prevention

  • Make flyweights immutable: All intrinsic fields must be final. No setters. Once created, a flyweight never changes — this is the single most important rule
  • Never store extrinsic state in flyweight: Everything context-specific is passed as method parameters — the flyweight object has zero awareness of who is using it or where
  • Use value-based equality: Override equals() and hashCode() on the flyweight based on intrinsic state — never rely on reference equality for flyweights
  • Bound the factory cache: Use a bounded cache (e.g., LinkedHashMap with LRU eviction or max size) to prevent unbounded growth if variety is high
  • Thread-safe factory: Use ConcurrentHashMap.computeIfAbsent() for atomic get-or-create operations — the standard safe approach in Java
Java — Break & Fix
// ❌ BREAKING — mutable intrinsic state in flyweight
public class BrokenTreeType {
    public String color; // ❌ mutable! One context changes it, all see the change
    public void setColor(String c) { this.color = c; } // ❌ NEVER do this
}
// Thread A: treeType.setColor("Red")  → all trees using this type turn red

// ✅ FIX — all intrinsic fields final, no setters
public class TreeType {
    private final String name;
    private final String color;   // ✅ final — never changes after creation
    private final String texture; // ✅ final
    // No setters
}

// ❌ BREAKING — storing extrinsic state in flyweight
public class BrokenTreeType {
    private int x, y; // ❌ extrinsic! Shared by all — overwrites per request
    public void draw() { /* uses this.x, this.y */ } // ❌ race condition
}

// ✅ FIX — extrinsic state passed as parameters, never stored
public void draw(int x, int y) { // ✅ x,y are parameters, not fields
    System.out.printf("Drawing %s at (%d,%d)%n", name, x, y);
}

// ❌ BREAKING — non-thread-safe factory
public static TreeType getTreeType(String key) {
    if (!cache.containsKey(key)) {      // ❌ check-then-act race condition
        cache.put(key, new TreeType(key)); // two threads create duplicates
    }
    return cache.get(key);
}

// ✅ FIX — atomic computeIfAbsent on ConcurrentHashMap
private static final ConcurrentHashMap<String, TreeType> cache
    = new ConcurrentHashMap<>();

public static TreeType getTreeType(String name, String color, String tex) {
    return cache.computeIfAbsent(name + "_" + color,
        k -> new TreeType(name, color, tex)); // ✅ atomic — thread-safe
}

How Other Patterns Relate

Interview Cheat Sheet

  1. What: Shares common state across many objects to reduce memory. Splits state into intrinsic (shared, stored in flyweight, immutable) and extrinsic (unique, passed as parameters). One flyweight serves many contexts.
  2. How: ConcreteFlyweight with final intrinsic fields. FlyweightFactory with a cache map — returns existing or creates new. Context stores extrinsic state + flyweight reference. Flyweight method takes extrinsic state as parameters.
  3. Critical rule: Flyweights MUST be immutable. Any mutable shared state corrupts all contexts sharing that flyweight. Use ConcurrentHashMap.computeIfAbsent() for thread-safe factory creation.