How do you decide which fields of an object are intrinsic (shareable) versus extrinsic (per-context) when applying the Flyweight pattern, and what happens if you get the split wrong?
answer
- Same value in every context?
- Substitutability: swap occurrences, nothing changes
- "Which one" data is always extrinsic
- Extrinsic-in-flyweight = cross-talk bug
- High cardinality kills sharing (K → N)
basics
~20 sAsk: "would this value be the same for every place the object is used?" If yes it is intrinsic and can be shared; if it changes per use (position, owner, timestamp) it is extrinsic and must be passed in. Putting per-use data in the shared object corrupts all users.
solid answer
~60 sThe test is context-independence, not merely "does this value repeat". A field is intrinsic if two occurrences with the same value are genuinely interchangeable everywhere in the program: glyph shape, tree species mesh, tax rule for a country, chess-piece movement rules. A field is extrinsic if it identifies *which* occurrence you are talking about: coordinates, board square, row index, selection state, per-instance mutable status. Getting it wrong fails in two directions. Marking extrinsic data as intrinsic is a correctness bug: because one instance is shared by unrelated clients, a per-use value written into it becomes visible to all of them ("every tree jumps to the last tree's position"), and it also explodes `K`, the number of distinct flyweights, until the cache is bigger than the objects it replaced. Marking intrinsic data as extrinsic is only a design smell: it still works, but you thread more parameters through call sites and lose sharing you could have had. When a field is *sometimes* shared, prefer computing it from extrinsic inputs, or split the flyweight into finer-grained ones.
code
pseudocode · 14 lines// intrinsic: shared, immutable, no position
immutable class TreeKind(mesh, barkTexture, leafTexture) {
fun draw(canvas, x, y, scale) { /* extrinsic passed in */ }
}
factory TreeKinds {
private map cache // key -> TreeKind
fun of(name) = cache.computeIfAbsent(name) { TreeKind(load(name)...) }
}
// extrinsic: one compact record per occurrence
struct Tree(kindRef, x, y, scale)
for (t in forest) t.kindRef.draw(canvas, t.x, t.y, t.scale)go deeper
State the plain test — same in every context = shared; changes per use = passed in — and give one example pair such as glyph shape vs position.
Give the substitutability test, classify a concrete model's fields, and explain that stuffing per-use data into a shared object corrupts every user.
Add the cardinality dimension (K vs N), splitting flyweights so distinct counts add rather than multiply, unshared concrete flyweights for the exceptional cases, and the requirement that extrinsic state itself be stored compactly.
Discuss it as a representation decision with a measurable cost model, its interaction with concurrency and data-oriented layouts, and when to move deduplication below the API (interning at the storage or serialization layer) instead of reshaping the domain model.
## The decision test When applying **Flyweight** — the pattern that stores one shared, immutable copy of the repeated part of an object and passes the varying part in from outside — the whole design collapses to one classification decision per field. Ask of each field, in order: 1. **Is it context-independent?** Would the value be identical for *every* place this logical object appears? A glyph outline for `a` in Times 12pt is the same everywhere on the page → intrinsic. A glyph's page position is different at every appearance → extrinsic. 2. **Would two occurrences with equal values be interchangeable?** If I swapped occurrence A's object for occurrence B's object and nothing observable changed, the differing fields are all extrinsic and the rest is intrinsic. This is the *substitutability* form of the test and is the sharpest one. 3. **Does it participate in identity of the *occurrence* rather than of the *value*?** "Which one" data (index, coordinate, owner, id, selection flag) is extrinsic by definition. 4. **Does it change over time independently per occurrence?** Anything mutable per occurrence must be extrinsic; shared state must be immutable. 5. **How many distinct values does it take?** Even a genuinely context-free field can be a bad intrinsic candidate if it is high-cardinality, because it multiplies `K` (the number of distinct flyweights). A timestamp is context-free in the sense that it does not depend on rendering position, yet including it makes almost every flyweight distinct and destroys sharing. ## Worked examples | Domain | Intrinsic (shared) | Extrinsic (passed in) | |---|---|---| | Text rendering | glyph outline, font family/size/weight, advance width | x/y position, colour if per-run, selection highlight | | Forest scene | species mesh, bark/leaf textures, LOD data | position, rotation, scale, health, age | | Chess | piece type + movement rules + sprite | current square, has-moved flag, owner if not encoded in type | | Tabular data | the distinct string value in a dictionary-encoded column | the row number(s) holding the code | | Tax engine | rate table + rounding rules for a jurisdiction | the invoice amount and date being computed | ## Failure mode 1 — extrinsic data smuggled into the flyweight This is a **correctness** failure, and it is the classic Flyweight bug. Suppose `TreeType` holds `mesh`, `texture` and — mistakenly — `x`, `y`. The factory returns the *same* `TreeType` instance for every oak, so setting `x`/`y` before each draw makes all oaks render at the last position set, and under concurrency two threads drawing different oaks interleave writes and produce garbage or torn output. Symptoms are "impossible" cross-talk bugs between unrelated parts of the system, usually only visible at scale or under load. A secondary symptom is cache blow-up: if `x`,`y` are part of the factory key, `K` approaches `N` and the flyweight map costs more than the naive objects. ## Failure mode 2 — intrinsic data pushed out as extrinsic This is only a **design/ergonomics** failure. Passing the font metrics on every `draw` call works and stays correct; it just makes signatures wide, invites callers to pass inconsistent values, and forfeits sharing. It is the safer direction to err in, and a reasonable first step when you are unsure — you can pull a field back into the flyweight later once you have measured its cardinality. ## Handling the "sometimes shared" field Real models rarely split cleanly. Three techniques: - **Derive it.** If colour is usually one of eight theme colours, keep the eight as flyweights and compute the rare custom colour at the call site. - **Split the flyweight.** Make `GlyphShape` and `TextStyle` separate flyweights and compose them per occurrence, so a change along one axis does not multiply the other. This keeps `K` additive (`K1 + K2`) instead of multiplicative (`K1 × K2`). - **Two-tier / unshared flyweights.** The original pattern explicitly allows *unshared concrete flyweights*: objects that implement the same interface but are not interned, used for the minority of occurrences that genuinely need private state. Clients call them identically. ## Where extrinsic state actually lives An under-discussed point: Flyweight only pays off if the extrinsic state is stored *compactly*. If each occurrence becomes a heavyweight `TreeInstance` object with a header, a reference to the flyweight and three boxed doubles, you may have moved the bloat rather than removed it. Typical compact representations are parallel arrays (structure-of-arrays), packed structs/records, or an index into a column store. This is why Flyweight and data-oriented layouts show up together.
- A field is context-free but has a million distinct values. Should it be intrinsic?Usually no. Sharing only pays when occurrences ≫ distinct values. A high-cardinality field drives the number of distinct flyweights toward the number of occurrences, so you pay for the cache and gain nothing. Either leave it extrinsic or split it into its own flyweight dimension if it repeats along a different axis.
- How would you detect that someone stored extrinsic state inside a shared flyweight?Signals: the flyweight class has setters or non-final fields; the client mutates the object returned by the factory before each use; bugs appear only at scale or under concurrency and look like unrelated occurrences interfering. Enforce by making the flyweight deeply immutable and, in tests, asserting the factory returns the identical instance for equal keys.
A rubber stamp carries the design (intrinsic); the ink pad position on the page is decided per press (extrinsic). Carving the page coordinates into the stamp would be absurd — and that is exactly the bug when someone stores position inside a shared flyweight.
saying these in an interview costs you the question
- Treating 'this value repeats a lot' as sufficient for intrinsic, ignoring context-dependence
- Adding position/index/owner to the factory key, which drives distinct flyweights toward the occurrence count
- Setting a field on the shared flyweight just before each use ('set position, then draw')
- Assuming the split must be all-or-nothing instead of using unshared concrete flyweights for exceptional cases
- Declaring victory after sharing intrinsic state while each occurrence remains a fat object with its own header