Hermes Wiki

Flyweight Pattern

Concept

Flyweight cuts memory use when a program needs a very large number of similar objects by splitting each object's state into two categories and sharing one of them. Intrinsic state is the data that's identical across many instances and never changes — a character glyph's font and shape data, a tree species' texture and mesh in a forest-rendering scene, a tile type's sprite in a game map. Extrinsic state is what's actually unique per instance — a glyph's position on the page, a tree's coordinates and rotation, a tile's map position. The pattern factors intrinsic state out into a small number of shared "flyweight" objects (one per distinct combination of intrinsic state, typically held in a factory/cache keyed by that combination), and every logical instance becomes just a reference to the appropriate shared flyweight plus its own small extrinsic state passed in at the point of use, rather than a full standalone object.

The memory saving comes directly from the ratio: rendering a million trees in a forest doesn't need a million copies of the mesh and texture data (which might be megabytes each) — it needs one shared mesh/texture object referenced a million times, plus a million lightweight (x, y, z, rotation) tuples. The pattern only pays off when that ratio is favorable — many instances, and intrinsic state that's large relative to extrinsic state. A classic textbook example is a text editor representing each character on the page as an object: instead of one object per character carrying its own font/style/glyph data, the editor shares one flyweight per distinct (character, font, size, style) combination — of which there are only a few dozen or hundred, however long the document is — and each position on the page holds a reference to the shared flyweight plus its own extrinsic position.

Tradeoffs

Approach Benefit Cost
Flyweight (shared intrinsic state) Memory use scales with the number of distinct intrinsic-state combinations, not the number of instances — can be orders of magnitude smaller when instances are numerous and highly repetitive Adds a factory/cache layer to manage; extrinsic state must be threaded through every call that needs it instead of being a plain instance field, complicating call signatures
One full object per instance (no sharing) Simplest code — no factory, no intrinsic/extrinsic split, state lives naturally on the instance Memory scales linearly with instance count; at large scale (millions of similar small objects) this can exhaust memory or blow past cache-friendliness even when total conceptual data is small
Object pooling (reuse full objects, not split state) Also reduces allocation/GC churn, simpler mental model than intrinsic/extrinsic splitting Solves allocation rate, not steady-state memory — a pool of a million full objects still holds a million copies of state that Flyweight would have shared
Precompute and memoize expensive derived values without a formal factory Lightweight, no pattern ceremony Ad hoc caching without the intrinsic/extrinsic discipline tends to leak — nothing forces a clear boundary between what's shared and what's per-instance, so the split degrades as the code grows

The most important cost is easy to underweight: Flyweight fundamentally trades code complexity (an extra indirection layer, extrinsic state now threaded as parameters instead of living naturally on objects) for memory. That trade is only worth making when the instance count is genuinely large and the intrinsic/extrinsic ratio is genuinely favorable — applying it preemptively to a data set of a few thousand objects with modest per-object footprint adds real complexity for savings nobody will ever measure.

When to use / when not to

  • Use when a system needs to represent a very large number of objects (thousands to millions) that share most of their state, and that shared state is large relative to what's actually unique per instance — text rendering, particle systems, map/tile-based games, and large graph/network visualizations are the classic domains.
  • Especially valuable when profiling has actually shown memory pressure traceable to object count — this is a pattern to reach for in response to a measured problem, not preemptively.
  • Don't use it for a modest number of objects, or where per-instance state dominates over shared state — the factory/cache overhead and the extrinsic-state-threading complexity cost more in code clarity than the memory saved is worth.
  • Don't use it when intrinsic state isn't actually immutable — sharing a flyweight across instances is only safe if nothing ever mutates the shared object; if intrinsic state needs to change per logical instance under some circumstance, it isn't truly intrinsic and the split doesn't hold.

Common pitfall

Sharing state that looks safe to share but has a mutable field discovered only much later, at which point one caller's mutation silently corrupts every other "instance" referencing the same flyweight — since they're all the same underlying object. This is Flyweight's sharpest failure mode: the pattern's entire safety argument rests on intrinsic state being genuinely immutable, and a single field added later for a seemingly unrelated reason (a cache flag, a computed-and-memoized value that's supposed to be per-call but gets stored on the shared object instead) can reintroduce exactly the object-identity bugs the pattern was meant to avoid, and the bug manifests as a distant, unrelated-looking symptom rather than an obvious crash at the mutation site.

Engineering Lens

Flyweight is a pattern to justify with a number, not intuition — "we render up to 2 million particles per frame and profiling shows 40% of frame memory is duplicated particle-type data" is a real justification; "this might get large someday" is not, and applying the intrinsic/extrinsic split preemptively adds real API friction (every call site that used to just read a field now needs the extrinsic value passed in) for a payoff that may never materialize. The design review question worth asking isn't whether the pattern is correctly implemented structurally, but whether the intrinsic/extrinsic boundary was drawn correctly and will hold — specifically, whether anything classified as intrinsic could plausibly need to become per-instance later, since that's the change that breaks the pattern's safety guarantee rather than just its performance benefit.

  • Composite Pattern — Flyweight is frequently combined with Composite in practice (e.g. a scene graph of many leaf nodes sharing flyweight-rendered geometry), though the two patterns solve unrelated problems
  • Singleton Pattern — both patterns control object creation to avoid duplication, but Singleton guarantees exactly one instance total while Flyweight deliberately allows many shared instances, one per distinct intrinsic-state combination

Sources

Hermes Wiki