Skip to content

Flyweight

Intent

Support very large numbers of fine-grained objects by splitting their state in two: the part that repeats (intrinsic) lives once in an immutable shared object handed out by a factory, and the part that differs per use (extrinsic) is computed or stored outside and passed in when the object does its work. A document with a million characters needs one glyph object per distinct character-and-style pair, not one per character.

When to use and when not to

Use it when

  • You have many objects and most of their state is repeated: characters in a document, tiles on a map, particles, sprites, pieces on a board, markers on a chart. A 1 MB plain-text file is about 2^20, so roughly a million characters at 1 byte each; one style object per character is a million objects for three distinct styles.
  • The repeated state is immutable, or can be made so. Sharing anything mutable is a bug generator, not an optimisation.
  • The varying state is cheap to pass or compute: a position from an index, a square from a board lookup.
  • You measured: tracemalloc showed that the objects are the problem.

Leave it out when

  • The objects are few. A flyweight over two hundred objects is ceremony with no payoff.
  • Most state is unique per object; then there is nothing to share and the factory only adds a lookup.
  • You want to avoid construction cost rather than memory: that is a cache or an Object Pool, and the objects usually stay mutable and exclusive.
  • The shared object would need a parent pointer or any per-use field. The moment a flyweight stores its position, it stops being shareable.

Structure

Four roles: the immutable Flyweight, the Factory that pools it by key, a second intrinsic value it references, and the Client that keeps the extrinsic state and passes it in.

classDiagram
    class TextStyle {
        +font: str
        +size: int
        +bold: bool
        +italic: bool
        +colour: str
    }
    class Glyph {
        +char: str
        +style: TextStyle
        +draw(row: int, col: int) str
    }
    class GlyphFactory {
        -_pool: Dict~tuple,Glyph~
        -_lock: Lock
        +get(char: str, style: TextStyle) Glyph
        +__len__() int
    }
    class Document {
        -_factory: GlyphFactory
        -_glyphs: List~Glyph~
        -_width: int
        +insert(text: str, style: TextStyle) None
        +layout() Iterator~tuple~
        +draw(start: int, count: int) List~str~
        +distinct_glyphs() int
    }
    GlyphFactory "1" o-- "many" Glyph : pools
    Glyph --> TextStyle : intrinsic
    Document --> GlyphFactory : asks
    Document "1" o-- "many" Glyph : references, shared

Glyph.draw takes row and col as arguments and stores neither. Document holds a list of references, many of them to the same object, and derives the position from the index at layout time.

Canonical example in Python

The flyweight and its factory (code/patterns/flyweight.py, tested by code/patterns/tests/test_flyweight.py):

code/patterns/flyweight.py — intrinsic state, the Flyweight and its Factory
@dataclass(frozen=True, slots=True)
class TextStyle:
    """Intrinsic state: immutable, so one instance can be shared by every glyph that uses it."""

    font: str
    size: int
    bold: bool = False
    italic: bool = False
    colour: str = "black"

    def __post_init__(self) -> None:
        if self.size <= 0:
            raise ValidationError("font size must be positive")

    def __str__(self) -> str:
        flags = ("b" if self.bold else "") + ("i" if self.italic else "")
        return f"{self.font} {self.size}{flags} {self.colour}"


@dataclass(frozen=True, slots=True)
class Glyph:
    """The Flyweight: one character in one style. Everything about *where* it goes is passed in."""

    char: str
    style: TextStyle

    def draw(self, row: int, col: int) -> str:
        """The position is extrinsic: it arrives as arguments and is never stored on the glyph."""
        return f"{self.char!r} at ({row},{col}) in {self.style}"


class GlyphFactory:
    """The Flyweight Factory: the pool that turns equal arguments into the identical object.

    The lock makes check-then-create atomic. Without it two threads could each
    build a glyph for the same key, and one of them would keep an unshared copy
    that nothing else ever sees again.
    """

    def __init__(self) -> None:
        self._pool: dict[tuple[str, TextStyle], Glyph] = {}
        self._lock = threading.Lock()

    def get(self, char: str, style: TextStyle) -> Glyph:
        if len(char) != 1:
            raise ValidationError("a glyph is exactly one character")
        key = (char, style)
        with self._lock:
            glyph = self._pool.get(key)
            if glyph is None:
                glyph = self._pool[key] = Glyph(char, style)
            return glyph

    def __len__(self) -> int:
        return len(self._pool)

The client keeps what varies:

code/patterns/flyweight.py — the client computes the extrinsic state
class Document:
    """The client: a run of shared glyphs. Positions are computed, never stored per character."""

    def __init__(self, factory: GlyphFactory, width: int = 40) -> None:
        if width <= 0:
            raise ValidationError("width must be positive")
        self._factory = factory
        self._glyphs: list[Glyph] = []
        self._width = width

    def insert(self, text: str, style: TextStyle) -> None:
        self._glyphs.extend(self._factory.get(char, style) for char in text)

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

    def glyph_at(self, index: int) -> Glyph:
        return self._glyphs[index]

    def distinct_glyphs(self) -> int:
        return len({id(glyph) for glyph in self._glyphs})

    def layout(self) -> Iterator[tuple[int, int, Glyph]]:
        """Extrinsic state on the fly: row and column follow from the index and the width."""
        for index, glyph in enumerate(self._glyphs):
            row, col = divmod(index, self._width)
            yield row, col, glyph

    def draw(self, start: int = 0, count: int = 3) -> list[str]:
        return [glyph.draw(row, col) for row, col, glyph in islice(self.layout(), start, start + count)]

Four decisions to say out loud:

  • Split the state before you draw anything. Character and style are intrinsic: they are the same for every e in 12pt body text. Position is extrinsic: it differs for every occurrence and falls out of divmod(index, width), so the document stores an index, not coordinates.
  • Frozen dataclasses make the key and the contract. TextStyle is hashable because it is frozen, so (char, style) is a dict key; and frozen means a shared glyph cannot be changed under the other thousand references. The demo shows the FrozenInstanceError.
  • Equality is free; identity needs the pool. Glyph("e", body) == factory.get("e", body) holds without a factory. The memory saving comes from the is, and only the factory can promise it.
  • The factory's lock makes check-then-create atomic. Two threads asking for the same key at the same moment must get one object; the concurrency test fires sixteen workers at five keys and the pool ends at five.

Running python -m patterns.flyweight prints:

--- a document of hundreds of characters, built from a few dozen shared objects ---
characters: 909, glyph objects: 40, pool size: 40
the 'e' of 'the' on line one and on line two: same object -> True
--- extrinsic state: the position is computed at layout time and passed in ---
'F' at (0,0) in Helvetica 18b black
'l' at (0,1) in Helvetica 18b black
't' at (1,0) in Helvetica 12 black
--- shared objects must be immutable, and the dataclass enforces it ---
FrozenInstanceError: cannot assign to field 'char'
--- chess: 32 squares on the board, 12 piece objects in memory ---
squares: 32, objects: 12, cache: 12
e2 and d2 share one white pawn: True; symbols: rnbqkbnr
--- the interpreter does it too: small ints, interned strings, compiled patterns ---
int('256') is int('256'): True; int('257') is int('257'): False
two built strings: False; after sys.intern: True
re.compile returns the cached pattern: True

Pythonic variant

Python has two built-in flyweight factories. functools.cache on a constructor function is the pool, the lock and the lookup in one decorator, and Enum members are flyweights by construction: one object per member, shared by every reference, compared with is:

code/patterns/flyweight.py — Enum members and a cached constructor
class Colour(StrEnum):
    WHITE = "white"
    BLACK = "black"


class PieceKind(Enum):
    """Enum members are flyweights by construction: one object per member, shared by every reference."""

    KING = ("K", 0)
    QUEEN = ("Q", 9)
    ROOK = ("R", 5)
    BISHOP = ("B", 3)
    KNIGHT = ("N", 3)
    PAWN = ("P", 1)

    def __init__(self, symbol: str, points: int) -> None:
        self.symbol = symbol
        self.points = points


@dataclass(frozen=True, slots=True)
class Piece:
    """Intrinsic: kind and colour. The square it stands on belongs to the board, not to the piece."""

    kind: PieceKind
    colour: Colour

    @property
    def symbol(self) -> str:
        return self.kind.symbol if self.colour is Colour.WHITE else self.kind.symbol.lower()


@cache
def piece(kind: PieceKind, colour: Colour) -> Piece:
    """``functools.cache`` on a constructor function is a flyweight factory in one line."""
    return Piece(kind, colour)


def starting_board() -> dict[str, Piece]:
    """32 squares, 12 objects: the board maps extrinsic squares to intrinsic pieces."""
    back_rank = (
        PieceKind.ROOK, PieceKind.KNIGHT, PieceKind.BISHOP, PieceKind.QUEEN,
        PieceKind.KING, PieceKind.BISHOP, PieceKind.KNIGHT, PieceKind.ROOK,
    )
    board: dict[str, Piece] = {}
    for file, kind in zip("abcdefgh", back_rank, strict=True):
        board[f"{file}1"] = piece(kind, Colour.WHITE)
        board[f"{file}2"] = piece(PieceKind.PAWN, Colour.WHITE)
        board[f"{file}7"] = piece(PieceKind.PAWN, Colour.BLACK)
        board[f"{file}8"] = piece(kind, Colour.BLACK)
    return board

The board is a dict from square to piece: the square is extrinsic and belongs to the board, the kind and colour are intrinsic and belong to twelve shared Piece objects. Moving a piece is a dict update; no piece object ever changes.

Reach for When
An Enum The set of flyweights is fixed and known when the code is written (piece kinds, cell types, log levels)
functools.cache on a constructor The key space is small and bounded; the cache may live for the process
A weakref.WeakValueDictionary pool The key space is unbounded and unused flyweights should be collectable
An explicit factory class with a lock You need __len__, eviction, statistics, or a test that asserts the pool size
sys.intern The flyweights are strings compared many times; identity makes == a pointer check

Real-world usage

  • CPython itself: small integers are pre-allocated and shared, identifiers and short strings are interned, None, True and False are singletons, and re.compile returns the cached pattern object for a repeated pattern.
  • enum.Enum: every member is a shared object; PieceKind.KING is PieceKind.KING is the whole point. datetime.timezone.utc and decimal contexts are shared immutable values in the same spirit.
  • functools.cache / lru_cache in front of any pure constructor turns it into a flyweight factory.
  • Frameworks and engines: sprite and texture atlases shared across thousands of game objects, glyph caches in every text renderer and browser, NumPy views sharing one buffer.
Looks like Flyweight How to tell them apart
Singleton One instance per class, reached globally. A flyweight is one instance per value, reached through a factory; a Singleton is the degenerate flyweight with a single key.
Object Pool Hands out exclusive, mutable objects and takes them back (connections, threads). A flyweight is shared and immutable, and nobody gives it back.
Prototype Clones to produce distinct copies you can then change. A flyweight returns the same object you must not change.
Cache / memoisation The same mechanism, a different purpose. A cache saves recomputation; a flyweight saves memory through identity. functools.cache serves both; call it a flyweight when sharing is the point.
Value object The prerequisite, not the pattern. Every flyweight is a value object; a value object becomes a flyweight when a factory guarantees one instance per value.
Composite The GoF text shares leaves of a large composite as flyweights; the price is that shared leaves cannot hold a parent pointer, so the walk must carry the context.

Where it appears in LLD problems

  • Design a text editor with undo and redo — characters and styles as shared glyphs; the undo stack stores indexes and references, not copies of the text.
  • Design chess — twelve shared piece objects, a board that maps squares to them, and move generation that takes the square as an argument.
  • Design the snake game — cell types and sprites shared across the grid; only coordinates change each tick.

Interview tips

Interview tip

Split the state out loud before naming the pattern: "character and style are intrinsic and immutable, so they go in a shared Glyph; position is extrinsic and comes from the index, so it is passed to draw." Then name the factory and its lock, give the Python shortcut (functools.cache on the constructor, or an Enum when the set is fixed) and say you would measure with tracemalloc before doing any of it.

Common mistake

Letting extrinsic state leak into the flyweight. A Glyph with a position field, or a mutable TextStyle whose colour gets changed for one heading, either destroys the sharing or changes every character that shares it. Keep flyweights frozen and keep per-use state with the client. Runner-up: reaching for the pattern at two hundred objects, where the lookup costs more than it saves.