Flyweight reduces memory by sharing the parts of many objects that are identical, keeping only what genuinely differs per instance. It is the one pattern in the catalogue that exists purely for a resource constraint, and that makes it the one most often applied without evidence.
Reach for it after a measurement, never before. The pattern trades clarity and mutability for memory, and a program that did not have a memory problem has simply become harder to read.
The split that defines it
Every field on an object is one of two kinds:
Intrinsic state is the same for many instances and independent of context — a character's glyph outline, a tree species' texture and name, a log level's label and severity. This can be shared.
Extrinsic state is unique per use and depends on context — where this character sits on the page, where this tree stands in the forest, when this log line was written. This cannot be shared, and must be passed in or stored elsewhere.
Diagram, as text
- 1,000,000 tree objects each with species name, texture, mesh, x, y, scale leads to Which fields are the same across many instances?
- Which fields are the same across many instances? leads to Intrinsic — shared 12 TreeSpecies objects (species, texture, mesh)
- Which fields are the same across many instances? leads to Extrinsic — per instance 1,000,000 small records (x, y, scale)
- Intrinsic — shared 12 TreeSpecies objects leads to Memory dominated by 3 numbers per tree
- Extrinsic — per instance 1,000,000 small records leads to Memory dominated by 3 numbers per tree
// Shared: one object per species, however many trees exist
class TreeSpecies {
constructor(name, texture, mesh) {
this.name = name; this.texture = texture; this.mesh = mesh;
Object.freeze(this);
}
}
const speciesCache = new Map();
function speciesFor(name, texture, mesh) {
if (!speciesCache.has(name)) speciesCache.set(name, new TreeSpecies(name, texture, mesh));
return speciesCache.get(name);
}
// Per instance: only what differs
class Tree {
constructor(x, y, scale, species) { this.x = x; this.y = y; this.scale = scale; this.species = species; }
}A million trees across twelve species holds twelve textures rather than a million. The saving is proportional to the ratio of instances to distinct intrinsic values, which is why the pattern is spectacular in the rare cases it fits and pointless everywhere else.
Immutability is not optional
The shared object is referenced by every instance using it. If it can be mutated, changing it for one changes it for all — the same aliasing failure described under Prototype, but affecting a million objects rather than two.
Freeze the shared object at construction. If the language cannot enforce that, expose no setters and copy anything mutable on the way in. A flyweight with a public mutable field is a bug that has not happened yet.
You already use it
String interning. Most runtimes share identical string literals so that a million occurrences of "active" are one object. This is Flyweight built into the language, and it is why identity comparison on strings is a trap in some languages — two equal strings may or may not be the same object.
Boxed integer caches. Small integers are commonly pre-allocated and shared, which produces the classic surprise that identity comparison works for small values and fails for large ones.
Symbols, enums and interned atoms. One object per distinct name, referenced everywhere.
Glyph rendering. The original motivating case: a document with a million characters holds one object per distinct character, not per occurrence.
Column-oriented storage. Dictionary encoding — storing distinct values once and referencing them by index — is Flyweight applied to data at rest, and it is why analytical databases compress categorical columns so well.
Why it is a last resort
It complicates the object model. Extrinsic state has to be passed to every method that needs it, so signatures grow: species.render(x, y, scale) instead of tree.render(). That noise appears everywhere the flyweight is used.
It forbids mutation. A perfectly reasonable future requirement — "let the user rename a species" — now has to consider every sharer.
The cache is a leak by default. A map from key to flyweight holds every entry forever. Unbounded key spaces need weak references or eviction, and getting this wrong replaces a memory problem with a worse one.
Cheaper options usually exist. Before restructuring, check whether the objects can simply be fewer, smaller, or absent:
- Do all million need to exist at once? Streaming or paging removes the problem rather than shrinking it.
- Is a compact representation available? A struct-of-arrays layout, or typed arrays instead of objects, often beats Flyweight and keeps the code simple.
- Is the memory actually in the objects? Profilers routinely show the real cost is elsewhere — a cache, a retained request buffer, a logging framework.
Check your understanding
Common mistakes
Applying it without measuring. The pattern costs clarity and mutability, and buys nothing if values do not repeat heavily.
A mutable shared object. One write, a million silently wrong instances, and no proximity between cause and symptom.
An unbounded cache. A leak introduced to fix a memory problem.
Splitting the state wrongly. A field that varies per instance living on the shared object means instances silently share a value they should not.
Ignoring cheaper options. Fewer objects, streaming, or a compact array layout usually beats Flyweight and keeps the code readable.
Relying on identity comparison. Whether two equal values are the same object depends on whether interning applied, which varies by runtime and by value.
Practice
A log-processing service holds 20 million parsed log events in memory and runs out. Each event has a timestamp, level, service name, host, message and a map of attributes. Propose an approach, in the order you would try things.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Look at which fields repeat across 20 million events, and ask first whether they all need to be resident.
Show a strong answer
First, question the premise, because it is usually the largest win available. Twenty million events resident at once is a design choice. If the work is a scan, an aggregation or a filter, streaming removes the problem rather than shrinking it, and no pattern is needed. I would establish what the events are actually for before optimising their representation — the answer often makes the rest of this unnecessary.
Then measure what the memory actually is. The instinct is that it is the events; the profile frequently says otherwise — retained request buffers, an oversized parser cache, the attribute maps rather than the events themselves. Optimising the wrong structure is the standard outcome of skipping this step.
Assuming the events are genuinely the cost, classify the fields by repetition:
level— a handful of distinct values across 20 million events. Extremely high ratio.serviceandhost— hundreds or a few thousand distinct values. Very high ratio.- Attribute keys — a bounded vocabulary repeated everywhere, even though the values vary. This is often the biggest single win and the one people miss, because the map is seen as one field.
timestamp— unique per event. Not shareable.message— mostly unique, though templates repeat if the messages are structured.
What I would do, cheapest first:
- Intern the low-cardinality fields — level, service, host, attribute keys. Each becomes a reference to one shared immutable object. This is Flyweight, it is a contained change, and it does not alter the event's interface.
- Replace the per-event attribute map with a shared schema plus a values array where events have a common shape. A map per event is a large fixed overhead 20 million times over; this is dictionary encoding and it usually beats interning alone.
- Only then consider a compact layout — parallel typed arrays rather than objects. This is a bigger rewrite and worth it only if the first two are insufficient.
The bounding I would insist on: host and attribute keys come from data, so their key space is not provably finite. A misbehaving client emitting unique hostnames would grow the intern table without limit — the classic case of the fix becoming the leak. Cap the table and fall back to unshared values past the cap.
What I would not do: intern the message field. High cardinality means the table approaches the event count, and the hashing cost is paid on every parse for almost no sharing.
An engineer proposes making a `Currency` class a flyweight — one shared instance per ISO code — arguing it saves memory. There are about 180 currencies and the system holds perhaps 50,000 Money objects. Evaluate the proposal.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Do the arithmetic, then ask whether there is a better reason to do it anyway.
Show a strong answer
The memory argument does not survive the arithmetic. Fifty thousand Money objects over 180 currencies is a ratio of under 300, and in practice far worse: most systems are concentrated in a handful of currencies, so the effective distinct count is maybe five. The saving is 50,000 small currency references collapsing to five objects — a few megabytes at most, on a system that clearly is not memory constrained or this would have arrived as an incident rather than a proposal. If the justification is memory, reject it: there is no measurement and the ratio is unimpressive.
But I would accept the change for a different and better reason: correctness and modelling.
- A currency is a value with identity in the domain. There is one Indian rupee. Having a hundred equal-but-distinct
Currencyobjects invites==comparisons that work by accident in tests and fail in production, and invites two "GBP" objects that disagree about the number of minor units. - A single canonical instance per code enforces the invariant that a currency's properties — code, minor units, symbol — are consistent everywhere. That is worth having regardless of memory.
- Construction becomes a validation point.
Currency.of('XYZ')can reject an unknown code at the boundary, where a public constructor cannot. - The key space is genuinely finite and known, so the cache-as-leak risk that makes Flyweight dangerous does not apply here at all.
So my answer is: do it, and change the argument. This is not a Flyweight for memory; it is a canonical value object that happens to share instances. That distinction matters because it changes what gets reviewed — the important properties are immutability, a private constructor, a lookup that rejects unknown codes, and equality by code — rather than an allocation count nobody will measure.
The general principle worth taking from this: when a pattern is proposed with a weak justification, check whether there is a strong one before rejecting the change. Structures are often right for reasons other than the ones offered, and reviewing the reason is as useful as reviewing the code.
Key takeaways
- Flyweight shares intrinsic state (identical across instances) and keeps extrinsic state (unique per use) outside.
- The saving is roughly instances ÷ distinct values — transformative at high repetition, worthless at low.
- Measure first. It is the one pattern that exists purely for a resource constraint, so applying it without evidence is pure cost.
- Shared objects must be immutable, enforced structurally. Mutation surfaces as unrelated objects being wrong, far from the write.
- The cache is a leak by default. Bound it unless the key space is provably finite.
- You already use it: string interning, boxed integer caches, enums and dictionary encoding in columnar stores.
- Cheaper alternatives usually exist: fewer objects, streaming, or a compact array layout.
- A canonical shared instance is sometimes right for modelling reasons rather than memory — currencies, enums, units.
Further reading
- Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
- Paul Calder and Mark Linton, "Glyphs: Flyweight Objects for User Interfaces" (ACM UIST, 1990) — the origin of the pattern.
- Daniel Abadi, Samuel Madden and Miguel Ferreira, "Integrating Compression and Execution in Column-Oriented Database Systems" (ACM SIGMOD, 2006) — dictionary encoding.
- Brian Goetz and others, Java Concurrency in Practice (Addison-Wesley, 2006) — immutability and safe sharing.
- Martin Fowler, Patterns of Enterprise Application Architecture (Addison-Wesley, 2002) — value objects and money.