bigonotes
CS ConceptsDesign Patterns

Flyweight Pattern

Save memory across huge numbers of similar objects by sharing their common, immutable state through a caching factory.

7 min readLast updated Design Patterns

A game forest with a million trees, a document with a million characters, an explosion with ten thousand particles: most of the data in each object is identical. Flyweight stores that shared data once and lets every object point to it, keeping only the truly unique bits (like position) per object.

Intent#

Use sharing to support large numbers of fine-grained objects efficiently. (GoF)

Analogy: a library. Instead of everyone buying their own copy of the same book, many readers share one copy.

The key move is to split an object's state in two:

Intrinsic state Extrinsic state
What Shared, context-independent data Unique, context-dependent data
Example (tree) Model, texture, leaf type Position (x, y), age, health
Mutability Immutable (it's shared) Can change
Where it lives Inside the flyweight In the context object, or passed in as arguments

The problem: duplicated heavy data#

When every object carries its own copy of the same assets, memory explodes:

# 100,000 soldiers, each carrying its own copy of the same assets
class Soldier:
    def __init__(self, name, x, y):
        self.name = name                  # unique
        self.x, self.y = x, y             # unique
        self.texture = load_texture()     # duplicate, say 50 MB
        self.model = load_model()         # duplicate, say 30 MB
        self.moves = load_animations()    # duplicate, say 20 MB
        self.sounds = load_sounds()       # duplicate, say 10 MB
# 100,000 × 110 MB ≈ 11 TB. Impossible.

Move the shared assets into one object per kind, and have each soldier point to it:

class SoldierType:  # ONE copy per kind of soldier
    def __init__(self):
        self.texture = load_texture()
        self.model = load_model()
        self.moves = load_animations()
        self.sounds = load_sounds()


class Soldier:  # 100,000 of these, each tiny
    def __init__(self, name, x, y, soldier_type: SoldierType):
        self.name = name
        self.x, self.y = x, y
        self.type = soldier_type  # a reference to the shared data
# ≈ 110 MB shared + 100,000 small objects

(The sizes are illustrative, but the arithmetic shows why the pattern exists.)

Structure#

Flyweight has four participants:

Participant Role
Flyweight interface Methods that accept extrinsic state as parameters (render(x, y))
Concrete flyweight Stores intrinsic state; immutable and shared
Flyweight factory Caches flyweights by key: returns the existing one or creates it once
Context Stores extrinsic state plus a reference to a flyweight, and passes that state in when calling it

The client asks the factory for a key and gets back either the existing flyweight or a newly created one. The context then pairs that flyweight with its own extrinsic state:

# 10 and 20 are extrinsic (this tree's position);
# the "Oak" TreeType is intrinsic (shared by every oak)
tree1 = Tree(10, 20, factory.get_tree_type("Oak"))

Example: a forest of a million trees#

A million trees share three tree types through a caching factory. __slots__ keeps each Tree small, since there are a million of them:

import random
from dataclasses import dataclass


# Concrete flyweight: intrinsic state only. frozen=True makes it
# immutable, so sharing it is safe.
@dataclass(frozen=True)
class TreeType:
    name: str
    color: str
    texture: str  # imagine a large bitmap
    model: str    # imagine a large mesh

    def __post_init__(self):
        print(f"Creating TreeType: {self.name} (heavy object)")

    # Extrinsic state comes in as parameters
    def render(self, x: int, y: int) -> None:
        print(f"Rendering {self.name} at ({x}, {y}) in {self.color}")


class TreeFactory:
    """Guarantees one flyweight per key."""

    _cache: dict[str, TreeType] = {}

    @classmethod
    def get_tree_type(cls, name: str, color: str, texture: str,
                      model: str) -> TreeType:
        key = f"{name}-{color}"
        if key not in cls._cache:
            cls._cache[key] = TreeType(name, color, texture, model)
        return cls._cache[key]

    @classmethod
    def count(cls) -> int:
        return len(cls._cache)


class Tree:
    """Context: extrinsic state + reference to the shared flyweight."""

    __slots__ = ("x", "y", "type")  # keep each of the million small

    def __init__(self, x: int, y: int, tree_type: TreeType):
        self.x = x
        self.y = y
        self.type = tree_type

    def render(self) -> None:
        self.type.render(self.x, self.y)  # pass extrinsic state in


class Forest:
    def __init__(self):
        self.trees: list[Tree] = []

    def plant_tree(self, name: str, color: str, texture: str,
                   model: str) -> None:
        tree_type = TreeFactory.get_tree_type(name, color, texture, model)
        x, y = random.randrange(1000), random.randrange(1000)
        self.trees.append(Tree(x, y, tree_type))


if __name__ == "__main__":
    forest = Forest()
    for _ in range(500_000):
        forest.plant_tree("Oak", "Green", "oak.png", "oak.obj")
    for _ in range(300_000):
        forest.plant_tree("Pine", "Dark Green", "pine.png", "pine.obj")
    for _ in range(200_000):
        forest.plant_tree("Birch", "White", "birch.png", "birch.obj")

    print("Trees:", len(forest.trees))         # 1000000
    print("TreeTypes:", TreeFactory.count())   # 3
    for tree in forest.trees[:3]:
        tree.render()

A million Tree objects hold two ints and a reference each; only three heavy TreeType objects exist.

More examples, same shape#

The same intrinsic/extrinsic split works far beyond trees.

Text editor glyphs. The flyweight is a glyph keyed by character + font + size; the context is each character's position and color.

class GlyphFactory:
    _glyphs: dict[tuple[str, str, int], "CharacterGlyph"] = {}

    @classmethod
    def get_glyph(cls, char: str, font: str,
                  size: int) -> "CharacterGlyph":
        key = (char, font, size)
        # Loads the bitmap once per key
        if key not in cls._glyphs:
            cls._glyphs[key] = CharacterGlyph(char, font, size)
        return cls._glyphs[key]


class DocumentCharacter:
    __slots__ = ("x", "y", "color", "glyph")

    def __init__(self, x: int, y: int, color: str,
                 glyph: "CharacterGlyph"):
        self.x, self.y = x, y  # extrinsic
        self.color = color     # extrinsic
        self.glyph = glyph     # shared

    def render(self) -> None:
        self.glyph.render(self.x, self.y, self.color)
# "Hello World", "Hello Again" (Arial 12) and "Hello There" (Times 14):
# 33 characters on screen, but each distinct (char, font, size)
# glyph is loaded only once.

Chess. 32 pieces share a handful of piece types (name, icon, value, movement rules). The key must include everything intrinsic: since white and black pieces have different icons, key by color + name (12 flyweights), or make the icon extrinsic and key by name alone (6 flyweights). Position is extrinsic and changes on every move.

def get_piece_type(color: str, name: str, icon: str, value: int,
                   moves: tuple[str, ...]) -> "PieceType":
    key = (color, name)
    if key not in piece_types:
        piece_types[key] = PieceType(name, icon, value, moves)
    return piece_types[key]

Particle systems. 10,000 fire, smoke and spark particles share three ParticleTypes (sprite, color, mass). Each Particle holds position, velocity and remaining lifespan, which update every frame; dead particles are removed, and the types live on.

Template#

This is the bare shape: an immutable flyweight and a factory that caches by key.

from dataclasses import dataclass


@dataclass(frozen=True)
class Flyweight:
    intrinsic_state: str  # shared, immutable

    def operation(self, extrinsic_state: str) -> None:
        print(f"{self.intrinsic_state} / {extrinsic_state}")


class FlyweightFactory:
    _pool: dict[str, Flyweight] = {}

    @classmethod
    def get(cls, key: str) -> Flyweight:
        return cls._pool.setdefault(key, Flyweight(key))


a1 = FlyweightFactory.get("TypeA")
a2 = FlyweightFactory.get("TypeA")
print(a1 is a2)  # True: same object
a1.operation("state 1")
a2.operation("state 2")

Real-world examples#

Python itself uses flyweights for common values:

  • String interning: sys.intern(s) returns one shared copy of a string, and CPython interns many identifier-like literals automatically.
  • Small-integer cache: CPython keeps one object for each integer from -5 to 256, so a = 100; b = 100; a is b is True. (It's an implementation detail: always compare numbers with ==.)
  • None, True, False and the empty tuple () are single shared objects.
  • Font rendering engines sharing glyphs.
  • Map icons for restaurants or gas stations, emoji images, music-notation symbols.
  • Business data: thousands of products sharing category or spec data, seats sharing a venue layout.

When to use it#

Flyweight fits when:

  • You have thousands or millions of similar objects.
  • Memory is a real, measured constraint (mobile, embedded, large games).
  • There's a clean split between shared (intrinsic) and unique (extrinsic) state.
  • The shared part can be immutable, and object identity doesn't matter.

When not to use it#

Skip it when:

  • You have tens of objects, not thousands. It isn't worth the complexity.
  • Nearly all state is unique per object.
  • The "shared" state needs to change per object.
  • No one has measured a memory problem yet (premature optimization).

Pitfalls#

Watch for these traps:

  • Mutable flyweights. Changing shared state changes it for every user. Make flyweights immutable (@dataclass(frozen=True), no setters).
  • Wrong cache key. The key must include all intrinsic state, or two different things collapse into one (the chess icon bug above).
  • Thread safety of the factory. With several threads, two can miss the cache at once and build the same flyweight twice. Guard the check-and-create with a threading.Lock.
  • Extra cost elsewhere. Factory lookups and passing extrinsic state around cost CPU, and the split makes code harder to read and debug.

Flyweight vs similar patterns#

Flyweight is easy to confuse with other patterns that manage instances:

Pattern Difference
Singleton One instance total; Flyweight has one shared instance per key
Object pool Lends out mutable objects temporarily (database connections); Flyweight shares immutable data permanently
Prototype Copies objects; Flyweight shares them
Factory Method Usually creates fresh objects; a flyweight factory returns cached ones
Composite About structure; large trees often use flyweights for their leaves

Key takeaways#

  • Split state into intrinsic (shared, immutable) and extrinsic (unique, passed in or stored in the context).
  • A factory with a cache ensures one flyweight per key.
  • The win scales with object count: a million trees, three tree types.
  • Make flyweights immutable and choose the cache key carefully.
  • Only reach for it when memory is a measured problem.

Found a mistake? Tell me on LinkedIn ↗

Further reading#