skip to content

Explain the Flyweight pattern's split between intrinsic and extrinsic state, and describe when the pattern actually pays for itself.

level: seniorimportance: nice to knowfreq 30%

answer

  1. intrinsic = shared + immutable; extrinsic = passed in
  2. factory pool keyed by intrinsic state
  3. millions of objects, few distinct kinds
  4. identity breaks: no reference equality
  5. string interning is Flyweight in the runtime

basics

~20 s

Flyweight saves memory by sharing one object across many logical occurrences. The shared part (intrinsic state) is context-independent and immutable, such as a character's glyph shape; the varying part (extrinsic state) such as position or colour is passed in by the caller instead of stored per instance.

solid answer

~50 s

Flyweight targets designs where a huge number of fine-grained objects would otherwise be created and most of their data is duplicated. You partition each object's state: intrinsic state is identical across occurrences and context-free (glyph outline, tree species mesh and texture, tile sprite) and lives in a shared, immutable flyweight; extrinsic state varies per occurrence (x/y, size, colour, owner) and is stored by the client or passed as arguments to flyweight operations. A factory keeps a pool keyed by intrinsic state and returns an existing instance rather than allocating a new one. The pattern pays off when object count is very large, duplicated intrinsic data dominates memory, extrinsic state is cheap to compute or store, and object identity is not meaningful to clients. Costs: flyweights must be immutable and thread-safe, extrinsic state passing complicates APIs, the pool must not leak, and CPU may be traded for memory.

code

pseudocode · 14 lines
pseudocode
// Intrinsic: shared, immutable
class Glyph(val charCode: Int, val outline: Path, val advance: Float) {
  fun draw(canvas, x, y, pxSize) { canvas.stroke(outline.scaled(pxSize), x, y) }  // extrinsic as args
}

object GlyphFactory {
  private val pool = ConcurrentMap<Int, Glyph>()
  fun get(charCode: Int) = pool.computeIfAbsent(charCode) { Glyph(it, loadOutline(it), advanceOf(it)) }
}

// Client stores only the extrinsic state per occurrence
for (occurrence in document) {
  GlyphFactory.get(occurrence.charCode).draw(canvas, occurrence.x, occurrence.y, occurrence.size)
}

go deeper

for a junior

Define the two kinds of state and say the shared, unchanging part lives in one object while the per-occurrence part is passed in; give the characters-in-a-document example.

for a middle

Add the factory pool, the immutability requirement, and the conditions under which the memory saving is real.

for a senior

Discuss pool lifetime and concurrency, key design, the CPU-for-memory trade, broken identity, and runtime-level equivalents such as string interning and boxed-value caches.

for a principal

Compare against data-oriented alternatives (struct-of-arrays, ECS, columnar storage), argue from measured memory profiles rather than pattern fit, and weigh the debuggability and API-ergonomics cost across a team.

## The problem Some designs want an object per occurrence of something tiny and numerous: every character in a document, every tile in a map, every particle in a simulation, every tree in a forest, every bullet, every cell in a spreadsheet. A per-occurrence object with full state costs allocation overhead plus duplicated fields, and at millions of occurrences that becomes the whole memory budget (and destroys cache locality and garbage-collection throughput). The observation behind Flyweight: most of that duplicated data is *the same* for all occurrences of a kind. A document with 100,000 letter `a`s needs one description of the glyph, not 100,000. ## The state split - **Intrinsic state** — independent of where or how the object is used; identical for every occurrence of that kind; therefore shareable. Must be **immutable**, because it is aliased by every user. Examples: glyph outline and metrics for a character code; mesh, texture and bark colour for a tree species; sprite bitmap for a map tile; the shared string content of an interned string. - **Extrinsic state** — depends on context; different per occurrence; therefore *not* stored in the shared object. Either kept by the client (in a parallel array, in the containing structure) or computed. Examples: x/y position, scale, rotation, current font size, selection/highlight status. Operations on a flyweight take the extrinsic state as parameters: `glyph.draw(canvas, x, y, size)` rather than `glyph.draw()`. ## The factory and the pool A **Flyweight Factory** owns a cache keyed by intrinsic state. `getFlyweight(key)` returns the existing instance if present, otherwise creates, stores and returns it. Clients must obtain flyweights only through the factory, otherwise sharing silently stops happening. Pool concerns: - **Concurrency** — the pool needs thread-safe access; a concurrent map or a per-key lock. Since flyweights are immutable, benign double-creation is usually acceptable. - **Lifetime and leaks** — an unbounded pool of rarely reused keys is a memory leak that outweighs the savings. Bound it (LRU) or use weak references where the runtime supports them. - **Key design** — the key must capture exactly the intrinsic state; a key that accidentally includes extrinsic data destroys sharing. ## When it pays for itself All of these should hold: 1. The application creates a **very large** number of objects (think 10^5–10^8, not 10^3). 2. **Storage cost is dominated by duplicated intrinsic data**, so removing it moves the needle. 3. **Distinct kinds are few relative to occurrences** — 100 tile types for a million tiles is ideal; a million distinct keys is pointless. 4. **Extrinsic state is cheap** to store elsewhere or recompute — otherwise you have merely moved the memory. 5. Clients **do not depend on object identity**, because two logically distinct occurrences are now the same instance. Reference equality, per-instance mutable fields, per-instance locks, and identity-keyed maps all break. ## Costs and edge cases - **Immutability is mandatory.** A mutable flyweight is a global variable in disguise; one writer corrupts every user. - **API pollution.** Threading extrinsic state through every call makes signatures wider and stateless code more awkward. It also makes the flyweight's methods context-free, which is often a design win but sometimes an ergonomic loss. - **CPU-for-memory trade.** Recomputing extrinsic state, or a factory lookup on a hot path, may cost more time than the memory saved is worth. Measure both dimensions. - **Debuggability.** A shared instance appears in many contexts at once; logging its state does not tell you which occurrence you are looking at. - **Language-level equivalents.** String interning, boxed-integer caches for small values, symbol/atom tables, interned enum-like value objects, texture atlases and glyph caches in graphics stacks are all Flyweight implemented by the runtime or library. Compilers and virtual machines do this automatically for some types, which is one reason hand-rolled flyweights are rarer than they used to be. - **Modern alternative.** Data-oriented layouts (struct-of-arrays, entity-component systems, columnar storage) often solve the same problem better by removing per-occurrence objects entirely rather than sharing them. Consider that before adding a pool. ## Relationship to other structural patterns - Frequently combined with **Composite**: leaves of a large tree (glyphs in a document, tiles in a scene graph) are flyweights, and the composite supplies extrinsic position. Shared leaves therefore cannot hold parent references. - Distinct from **Singleton**: Singleton is exactly one instance of a class for lifecycle/global-access reasons; Flyweight is many instances, one per distinct intrinsic value, for memory reasons. - Distinct from a plain **cache/memoization**: a cache is an optimisation over expensive computation and is transparently discardable; Flyweight is a design decision about where state lives, and clients are required to pass extrinsic state.

  • What breaks if a flyweight is mutable?
    Every occurrence sharing that instance sees the mutation, so a change intended for one logical object silently corrupts all of them, and concurrent writers create data races. Immutability is what makes sharing safe; if state must change per occurrence, it is extrinsic by definition.
  • How is Flyweight different from Singleton?
    Singleton restricts a class to exactly one instance for global access and lifecycle reasons. Flyweight creates many instances — one per distinct intrinsic value — purely to avoid duplicating memory, and it also prescribes moving context-dependent state out to the caller.
  • What modern alternative often beats Flyweight for large collections?
    Data-oriented layouts: store the occurrences as parallel arrays or columns of primitives (struct-of-arrays, entity-component systems, columnar formats) with an index into a small table of shared descriptors. That removes per-occurrence object overhead entirely and improves cache locality, rather than merely deduplicating it.

A theatre costume department. It stores one costume per character type (intrinsic) rather than one per performance; which actor wears it, on which night, in which scene, is recorded separately (extrinsic). It only saves anything because there are far more performances than costume designs.

saying these in an interview costs you the question

  • Describing Flyweight as 'just caching' — it prescribes a state split and moves extrinsic state to the caller.
  • Making the shared flyweight mutable.
  • Confusing Flyweight with Singleton (one instance total vs one per distinct value).
  • Applying it to a few thousand objects, where the pool and factory cost more than they save.
  • Forgetting that clients relying on object identity or per-instance locks will break.
  • Ignoring pool growth, turning the optimisation into a memory leak.

context