Flyweight Pattern
The Flyweight pattern is a structural pattern for situations where you must keep a very large number of objects in memory and most of those objects carry the same data over and over. Instead of letting every object own its own copy of that repeated data, you pull the shared part out into a single object that everyone points at. The classic motivation is a text editor: a document of a million characters does not need a million copies of the font, size, and style metadata — those repeat constantly and can be shared.
The pattern hinges on splitting an object's state into two kinds:
- Intrinsic state — the part that is shared and context-independent. It is stored inside the flyweight, set once, and treated as immutable. (Texture, shape, color of a particle; the glyph + font of a character.)
- Extrinsic state — the part that is unique to each use and is supplied by the caller at the moment of use, never stored in the flyweight. (Position, velocity, remaining lifespan of a particle.)
A flyweight factory hands out the shared objects. When a client asks for a flyweight with a given intrinsic state, the factory returns the existing instance if one already matches, and only creates a new one when nothing matches. That guarantee — same intrinsic state means same object reference — is the whole point.
What it actually saves in Java — being precise about the numbers
It is tempting to say "no flyweight = 140 bytes per particle, flyweight = 28 bytes, so 5× smaller." That headline is misleading on the JVM, and it is worth getting right, because the reason it is wrong is itself the lesson.
You cannot inline a String's contents into an object in Java. A field like String texture is an 8-byte reference (4 bytes with compressed oops) to a separate String object on the heap. So even the naive particle does not store 120 bytes of character data inline — it stores three references and points at three String objects that live elsewhere.
Here is the realistic picture for a Particle with four floats, one int, and three String fields, under a 64-bit HotSpot with compressed oops:
| Approach | Per-particle (object + fields) | Shared, once | For N = 100,000 |
|---|---|---|---|
| Naive: 3 String refs per particle | ≈ 12 B header + 16 B floats + 4 B int + 12 B (3 refs) + pad ≈ 48 B | String objects, only if literals/interned are reused | ≈ 4.8 MB of Particles |
| Flyweight: 1 ParticleType ref per particle | ≈ 12 B header + 16 B floats + 4 B int + 4 B (1 ref) + pad ≈ 40 B | One ParticleType (≈ 32 B) + its 3 Strings (≈ 120 B), shared by all N | ≈ 4.0 MB of Particles + ~150 B fixed |
Two honest takeaways. First, the per-object win here is modest — about 8 bytes, from holding one reference instead of three. The dramatic savings the pattern is famous for appear when the intrinsic data is large (a loaded texture bitmap, a parsed font, a geometry mesh) and would otherwise be duplicated per object. With three short strings the win is real but not 5×. Second, if those three strings are distinct String objects per particle (e.g. freshly parsed from network or file, not literals), the naive version also pays roughly 120 B × N for duplicated String objects — and that is the cost the flyweight collapses to a single ~150 B shared payload. The lesson: Flyweight saves you whenever the same heavy intrinsic payload would otherwise be duplicated, not because object fields get inlined.
Structure
- Flyweight — interface (or abstract class) declaring the operation, which takes the extrinsic state as a parameter.
- Concrete Flyweight — stores the immutable intrinsic state and implements the operation using both its intrinsic state and the passed-in extrinsic state. In the example below,
ParticleTypeplays this role. - Flyweight Factory — keeps a cache of flyweights and returns a shared instance for a given intrinsic key, creating one only on a cache miss.
- Client / Context — holds the extrinsic state and a reference to a flyweight. Here
Particleis the context (position, velocity, lifespan) andParticleSystemis the client.
Implementation (Java)
import java.util.*;
final class ParticleType { // Concrete Flyweight (immutable)
private static final Map<String, ParticleType> CACHE = new HashMap<>();
private final String texture, shape, color; // intrinsic state
private ParticleType(String texture, String shape, String color) {
this.texture = texture; this.shape = shape; this.color = color;
}
static ParticleType of(String texture, String shape, String color) { // Factory
String key = texture + '|' + shape + '|' + color;
return CACHE.computeIfAbsent(key, k -> new ParticleType(texture, shape, color));
}
String texture() { return texture; }
}
final class Particle { // Context: holds extrinsic state
private float x, y, vx, vy;
private int lifespan;
private final ParticleType type; // one shared reference
Particle(float x, float y, float vx, float vy, int lifespan, ParticleType type) {
this.x = x; this.y = y; this.vx = vx; this.vy = vy;
this.lifespan = lifespan; this.type = type;
}
void update() { x += vx; y += vy; lifespan--; }
void draw() { System.out.println("particle (" + x + "," + y + ") tex=" + type.texture()); }
}
final class ParticleSystem { // Client
private final List<Particle> particles = new ArrayList<>();
void add(float x, float y, float vx, float vy, int life,
String texture, String shape, String color) {
ParticleType type = ParticleType.of(texture, shape, color); // shared
particles.add(new Particle(x, y, vx, vy, life, type));
}
void simulate() { for (Particle p : particles) { p.update(); p.draw(); } }
}Implementation (Go)
Go has no class hierarchy; the flyweight is just a shared, immutable struct pointed at by many contexts, with a factory guarded for concurrent access.
package particles
import "sync"
type ParticleType struct { // flyweight: immutable intrinsic state
Texture, Shape, Color string
}
var (
cache = map[string]*ParticleType{}
mu sync.Mutex
)
func TypeOf(texture, shape, color string) *ParticleType { // factory
key := texture + "|" + shape + "|" + color
mu.Lock()
defer mu.Unlock()
if t, ok := cache[key]; ok {
return t // return shared instance
}
t := &ParticleType{texture, shape, color}
cache[key] = t
return t
}
type Particle struct { // context: extrinsic state + one pointer
X, Y, VX, VY float32
Lifespan int
Type *ParticleType // shared
}
func (p *Particle) Update() { p.X += p.VX; p.Y += p.VY; p.Lifespan-- }Flyweight vs. the alternatives you'd actually reach for
For a string-keyed shared cache like this, Flyweight is not the only tool — and on the JVM several built-in mechanisms overlap with it. Naming them and weighing them is how you justify reaching for the full pattern.
| Alternative | What it does | vs. Flyweight |
|---|---|---|
String.intern() / the string constant pool | Canonicalizes String values so equal strings share one backing object. | Dedupes the strings, but does nothing about the grouping. You still hold three references per particle and three String objects per distinct combination. Flyweight collapses the whole (texture, shape, color) tuple to one object reference and lets the flyweight carry heavy non-string payloads (bitmaps, meshes) too. Intern also has a fixed-size native pool and can stall if abused. |
-XX:+UseStringDeduplication (G1) | The GC transparently shares identical char[]/byte[] backing arrays across String objects. | Zero code, but it only touches the character arrays — you still pay for N String objects and N×3 references, and it is best-effort and GC-timed, not a structural guarantee. Use it as a free safety net, not a substitute when sharing must be deterministic. |
Shared enum | A fixed set of singleton instances chosen at compile time. | Strictly better than Flyweight when the set of intrinsic combinations is small and known up front — enum ParticleType { SMOKE, SPARK, MAGIC } gives you the same sharing with no cache, no factory, no key concatenation, and free identity equality. Choose Flyweight only when the combinations are open-ended or computed at runtime. |
| Plain object pooling | Reuses a pool of mutable objects to avoid allocation churn. | Solves a different problem: allocation/GC pressure, not duplicate data. Pooled objects are checked out, mutated, and returned; flyweights are immutable and shared concurrently with no checkout. Don't pool flyweights, and don't expect Flyweight to reduce allocation rate. |
Decision rule: small fixed set of intrinsic states → enum. Open-ended set, heavy/duplicated intrinsic payload, need for a guaranteed canonical instance → Flyweight. Only the strings repeat and you can't change the model → lean on interning or G1 string dedup first.
"Isn't this just a cache?" — the interviewer's favourite challenge. No: a Flyweight is structural sharing of immutable intrinsic state, where every context legitimately points at the same live object for the program's lifetime, and identity (==) is a guaranteed property you rely on. A general cache holds evictable copies of computed results keyed for latency; entries can be dropped and recomputed, and two lookups need not return the same reference. Flyweight's factory looks like a cache, but its contract is "one canonical instance per intrinsic key," not "a fast-path copy I may throw away."
When to use — and when not to
Use it when all of these hold: you have a very large number of objects; they would otherwise carry duplicated, heavy, immutable data; that data cleanly splits into an intrinsic part (sharable) and an extrinsic part (per-instance); and the set of distinct intrinsic states is far smaller than the number of objects.
Avoid it when any of these hold: the object count is modest (the indirection costs more than it saves); the intrinsic data is tiny (a few short strings or primitives — the per-object win may be a handful of bytes, as shown above); the "shared" state actually needs to mutate per object (then it isn't intrinsic); or a simpler tool fits — an enum for a fixed set, or string interning when only strings repeat.
Pitfalls
- Mutable flyweights. If a flyweight is mutable and one client changes it, every client sees the change. Intrinsic state must be immutable — make fields
finaland defensively copy any arrays. - Thread-unsafe factory. The cache is shared; guard it (
ConcurrentHashMap.computeIfAbsentin Java, a mutex in Go) or two threads racing on a miss may build two instances and break the identity guarantee. - Leaky caches. A factory that never evicts is a memory leak for unbounded key spaces. If keys are open-ended, bound the cache or use weak references.
- Confusing the two states. Putting extrinsic state in the flyweight silently corrupts shared data; forgetting to pass extrinsic state in makes every context look identical.
Cost model
Total memory ≈ (N × per-context cost) + (D × per-flyweight cost), where N is the number of objects and D is the number of distinct intrinsic states. The pattern wins precisely when D ≪ N and the per-flyweight payload is large. When D approaches N, you get all the complexity and none of the savings.
Source
Adapted and corrected from the Knowledge Guide lesson Flyweight Pattern (OO & Low-Level Design → Structural). The original text and particle-system example are from that lesson; the Java memory-model figures, the comparison against String.intern(), G1 -XX:+UseStringDeduplication, shared enum, and object pooling, and the corrected cost model were added in this revision. JVM object-layout figures assume 64-bit HotSpot with compressed ordinary object pointers (compressed oops); exact sizes vary by JVM, heap size, and alignment.
🤖 Don't fully get this? Learn it with Claude
Stuck on Flyweight Pattern? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Flyweight Pattern** (OO & Low-Level Design) and want to truly understand it. Explain Flyweight Pattern from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Flyweight Pattern** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Flyweight Pattern** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Flyweight Pattern** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.