Home › Low-Level Design › Flyweight Pattern
F

Flyweight Pattern

Flyweight is the pattern you reach for when you have millions of objects and they are eating your heap alive. Most of what those objects hold is identical, so stop copying it. Share one copy, pass the rest in. The pattern separates the engineers who think about memory from the ones who only think about classes, and interviewers probe it because getting it wrong looks exactly like getting it right until the heap graph tells on you.

Low-Level DesignHard11 min read

If you remember five things

The whole pattern in one glance. Everything below defends these five calls.

  1. Split the state. Intrinsic state (shared, stable, immutable) lives in the flyweight. Extrinsic state (per-use, like position) is passed in as a method argument. This split is the entire pattern.
  2. The factory is the cache. Ask for 'a' in Arial 12, get the cached instance if it exists, otherwise build and cache it. Same request, same object reference, provable with ==.
  3. Flyweights are immutable. They are shared, so one setter poisons every caller pointing at that reference. Build once in the constructor, no mutators.
  4. Only pay for it when N is large. Below thousands of objects the map lookup plus pointer hop is pure overhead. This is a premature-optimization magnet.
  5. Watch the pool cardinality. Bounded key space (letters, tree species) is fine. Unbounded keys turn the pool into a memory leak worse than the problem you set out to fix.

1The problem it solves#

Start with the pain, because the pattern only makes sense once you feel the waste.

  • Picture a text editor. Every character on screen is an object.
  • A Character holds its glyph, font family, font size, color, maybe hinting data, and its position (row, column).
  • A single document can have a million characters, so you allocate a million objects.
  • Each one carries a full copy of "font is Arial, size 12, color black."

Now do the arithmetic that should scare you.

  • Say the glyph payload is 200 bytes. Store it inline on every character.
  • One million characters times 200 bytes is roughly 200MB of pure duplication.
  • That is before object headers and the position fields you actually need.

The insight that unlocks it.

  • The glyph shape for 'a' in Arial 12pt black is the same no matter where it sits on the page.
  • Position changes. The look does not.
  • You are duplicating the expensive, stable part across every instance when you only need one of it.
  • Flyweight kills that duplication: one shared object for the stable data, every character points at it.
  • A million characters collapse to a handful of shared shapes plus a million cheap position records.
~200MB
The waste Flyweight erasesOne million characters at 200 bytes of inlined glyph data each. With sharing it drops to a tiny pool of unique glyphs plus a million tiny position records. That collapse is the entire reason the pattern exists.

2The idea (mental model)#

The whole pattern is one decision: split every object's state into two buckets.

  • Intrinsic state. Shared, never changes per instance: the glyph, the font, the sprite image, the tree texture. Lives inside the flyweight, immutable.
  • Extrinsic state. Unique to each use: position, the specific row and column, the x/y on a map. Does not live in the flyweight. The caller supplies it when it calls a method.

What that buys you.

  • Instead of a million fat Character objects, you get a handful of shared Glyph flyweights (one per unique letter plus font combo).
  • The position lives in the caller, in a tiny array or the loop variable.

The factory is where sharing actually happens.

  • Ask it for 'a' in Arial 12. If one already exists, it returns the cached one.
  • If not, it builds it, caches it, and returns it.
  • That cache is the whole game: two callers asking for the same thing get the exact same object reference.
  • You can prove it with ==, not just equals.
The one rule that keeps you out of trouble

Flyweights must be immutable. They are shared, so if one caller could mutate a flyweight it would corrupt it for everyone pointing at the same reference. No setters. Build it once in the constructor, done. The flyweight owns identity and appearance, nothing else.

3Structure#

The cast is small. Four roles, no more.

  • Flyweight. The interface or type that holds intrinsic state and exposes an operation taking extrinsic state as a parameter.
  • ConcreteFlyweight. The shared, immutable implementation.
  • FlyweightFactory. Owns the pool, hands back cached instances, creates on miss. A Factory with a cache bolted on, and that cache is what makes it a Flyweight factory rather than a plain one.
  • Client. Holds extrinsic state and asks the factory for flyweights.
The detail I look for in the code

Extrinsic state is a method parameter, not a field. That is the structural signature of the pattern. If you see render(x, y) instead of a stored this.x, you are looking at Flyweight. That one detail is how I tell a real implementation from a diagram someone memorized.

classDiagram class Glyph { <> +draw(x int, y int, color Color) void } class CharacterGlyph { -char symbol -Font font +draw(x int, y int, color Color) void } class GlyphFactory { -Map~String, Glyph~ pool +getGlyph(symbol char, font Font) Glyph } class TextDocument { -List~Placement~ placements +render() void } class Placement { -int x -int y -Color color -Glyph glyph } Glyph <|.. CharacterGlyph GlyphFactory o-- Glyph TextDocument *-- Placement Placement --> Glyph
Read the arrows and the whole argument falls out: many placements, one glyph. The document owns placements (composition, they die with it), each placement points at a shared glyph (association, not ownership, it does not control the glyph's lifetime), and the factory aggregates the pool of glyphs it hands out.

4Implementation in Java#

Build the text editor version and watch exactly where position lives.

// Intrinsic state lives here. Immutable. Shared.
public final class CharacterGlyph {
    private final char symbol;
    private final String fontFamily;
    private final int fontSize;

    public CharacterGlyph(char symbol, String fontFamily, int fontSize) {
        this.symbol = symbol;
        this.fontFamily = fontFamily;
        this.fontSize = fontSize;
    }

    // Extrinsic state (x, y, color) comes in as arguments.
    public void draw(int x, int y, String color) {
        System.out.printf("'%c' [%s %dpt] at (%d,%d) in %s%n",
                symbol, fontFamily, fontSize, x, y, color);
    }
}
// The factory is the cache. This is where sharing actually happens.
public class GlyphFactory {
    private final Map<String, CharacterGlyph> pool = new HashMap<>();

    public CharacterGlyph getGlyph(char symbol, String fontFamily, int fontSize) {
        String key = symbol + "|" + fontFamily + "|" + fontSize;
        return pool.computeIfAbsent(key,
                k -> new CharacterGlyph(symbol, fontFamily, fontSize));
    }

    public int pooledCount() {
        return pool.size();
    }
}
// The client holds extrinsic state and a reference to the shared flyweight.
public class Placement {
    private final int x, y;
    private final String color;
    private final CharacterGlyph glyph; // shared reference

    public Placement(int x, int y, String color, CharacterGlyph glyph) {
        this.x = x; this.y = y; this.color = color; this.glyph = glyph;
    }

    public void render() {
        glyph.draw(x, y, color);
    }
}
public class Demo {
    public static void main(String[] args) {
        GlyphFactory factory = new GlyphFactory();
        List<Placement> doc = new ArrayList<>();

        String text = "hello hello hello";
        int x = 0;
        for (char c : text.toCharArray()) {
            CharacterGlyph g = factory.getGlyph(c, "Arial", 12);
            doc.add(new Placement(x, 0, "black", g));
            x += 10;
        }

        doc.forEach(Placement::render);

        // 17 characters placed, but only the unique letters got allocated.
        System.out.println("Placements: " + doc.size());       // 17
        System.out.println("Glyphs in pool: " + factory.pooledCount()); // 5 (h e l o + space)
    }
}

What the numbers prove.

  • Seventeen placements, five glyphs. That ratio is the whole point.
  • Scale the document to a million characters and the pool stays tiny.
  • Pool size is bounded by unique letter-plus-font combos, not by document length.
  • The position objects are cheap. The shared glyphs carry the weight once.

Two things to say out loud when you write this.

  • CharacterGlyph is final with only final fields and no setters. That immutability is not decoration. It is what makes sharing safe. Point at it and say so.
  • This HashMap is unsafe if two threads call getGlyph at once. Swap to ConcurrentHashMap (which keeps computeIfAbsent atomic per key) once the editor goes multi-threaded.

5A real-world example#

The pattern is everywhere once you know the shape. Two that land well in a room.

Game engines live on this.

  • A forest with 100,000 trees. Each tree has a mesh, a texture, and a bark material.
  • Those are heavy and identical across trees of the same species.
  • Load one TreeType flyweight per species, keep it in a pool.
  • Every tree on the map is then just (x, y, scale, treeType).
  • The 3D data lives once, the transform lives per tree. That is how you render a dense world without exploding VRAM.

Java's own Integer.valueOf() is a flyweight.

  • Values from -128 to 127 are cached and returned as shared instances.
  • So Integer.valueOf(100) == Integer.valueOf(100) is true but Integer.valueOf(1000) == Integer.valueOf(1000) is false.
  • Same idea with String literals in the string pool.
  • The cache upper bound is tunable via the java.lang.Integer.IntegerCache.high property. Knowing that reads as "I have actually looked at this."

In LLD problem land, the tell is always the same: huge N, lots of repeated heavy state.

  • A Snake and Ladder or board game design (one shared Cell type, position passed in).
  • A map or tile rendering system.
  • Particle systems.
  • Any "simulate N million entities" prompt.

6When to use it, and when not to#

Reach for it only when every one of these holds.

  • You have a genuinely large number of objects. Thousands or millions, not dozens.
  • Memory is actually a constraint you can point to.
  • A big chunk of each object's state is identical across instances and can be made immutable.
  • You can cleanly pull the unique state out and pass it as arguments.

Skip it when any of these is true.

  • N is small. The factory, the cache, the indirection: all overhead you never earn back. A hundred objects do not need Flyweight. Leave them alone.
  • The shared state is cheap. If the "heavy" part is two ints, you added a HashMap lookup to save nothing.
  • The state is mostly unique per instance. If extrinsic state is 90% of the object, there is nothing to share. Flyweight gives you negative value here.
Pause before you read on: a candidate slaps Flyweight on a 50-object system because it "scales better." Why is that the wrong call, and what does it cost at that size?
Reveal the answer
It scales worse until N is large. Every single access now pays the indirection cost: a map lookup plus a pointer hop, on an object set small enough to fit in cache for free. You bought complexity (a factory, a pool, immutability constraints) to save memory that was never a problem. Measure, or at least justify the N out loud, before you reach for it. Reaching for it unprompted on a tiny system is a red flag, not a strength.
The misuse that fails you

Flyweight is a premature-optimization magnet. Slapping it on a small system because it "scales better" is a reject. It scales worse until N is large, because you pay the map lookup plus pointer hop on every access. If you cannot name the N that justifies it, you do not understand the pattern.

7Common mistakes#

These are the five I watch for, and the first one is the killer.

  • Mutable flyweights. The number one bug. Someone adds a setter or a mutable field, one caller changes it, and every caller sharing that reference sees the change. Action at a distance across your whole system, and it stays hidden until two callers happen to share. Flyweights are immutable. Full stop.
  • Storing extrinsic state inside the flyweight. If position creeps back in "for convenience," you undid the pattern. Two callers at different positions now need different objects, so you cannot share. Passing position as an argument is the discipline that is the pattern.
  • A factory that never evicts. The pool grows forever. For a bounded key space (letters, tree species) that is fine, it stays small. For an unbounded key space you built a memory leak worse than the problem you set out to fix. Know your key cardinality, and if it is unbounded, cap the pool with an LRU or use WeakReference values so dead flyweights can be collected.
  • Weak keys. Build the cache key wrong and two different intrinsic states collide, or the same state generates two keys and you get duplicate flyweights. The key must capture exactly the intrinsic state and nothing else, and it should implement equals and hashCode correctly if it is a composite object rather than a string.
  • Confusing it with object pooling. A connection pool reuses objects across time (borrow, return, reuse). Flyweight shares one immutable object across space (many callers point at it at once, nobody returns it). Different intent. Interviewers love to probe this, so have the one-liner ready.

8Where it shows up in interviews#

Here is the flow I expect a strong candidate to walk me through, and to draw: client asks the factory, factory checks the pool, returns shared or creates.

sequenceDiagram participant Client participant Factory participant Pool Client->>Factory: getGlyph('a', Arial, 12) Factory->>Pool: lookup key "a|Arial|12" alt cache hit Pool-->>Factory: existing glyph else cache miss Factory->>Factory: new CharacterGlyph(...) Factory->>Pool: store glyph end Factory-->>Client: shared glyph reference Client->>Client: draw(x, y, color)
The cache-hit branch is the common case and the whole payoff: most requests never allocate, they hand back a reference that already exists. The miss branch (build, store, return) runs once per unique intrinsic state and never again.

What interviewers actually test:

  • Can you name intrinsic vs extrinsic state for a given object? The real question. Split the state correctly and you understand the pattern. If you cannot, you memorized a diagram.
  • Why immutable? Expect the follow-up. Because the reference is shared, mutation corrupts every holder. One wrong setter poisons a million call sites.
  • Flyweight vs object pool. Know the one-liner: Flyweight shares immutable state across many users at once, a pool recycles objects over time for one user at a time.
  • Where is the memory win? Be ready for the back-of-envelope math: a million characters, each glyph 200 bytes, so without sharing that is ~200MB, with sharing it is a tiny pool times 200 bytes plus a million tiny position records. Doing that math on the board is what sells it.
  • A built-in example. Integer.valueOf caching -128 to 127. Nailing this signals you have seen it in the wild, not just a textbook.

State the split, justify the N, defend immutability. Do those three and you have shown me everything.

Follow-ups the interviewer will ask (and your answers)
"Two threads call getGlyph at the same time. What breaks?"
A plain HashMap can corrupt or lose an entry under concurrent writes, and computeIfAbsent is not atomic on it. Swap to ConcurrentHashMap, where computeIfAbsent is atomic per key, so two threads racing for the same glyph still end up sharing one instance rather than building two. The flyweights themselves are immutable, so once built they are safe to read from any thread.
"How is this different from an object pool?"
Different axis. An object pool recycles mutable objects over time: one user borrows, uses, and returns an object for the next user. Flyweight shares one immutable object across space: many callers point at the same reference at once and nobody ever returns it. Pool is about reuse to avoid allocation cost. Flyweight is about sharing to avoid duplication.
"Your key space is unbounded. Now the pool is a leak. Fix it."
Cap it. Either an LRU eviction policy so the pool holds only the hottest flyweights, or WeakReference values so a flyweight no caller still references can be garbage collected. First, though, I would question whether the key space is truly unbounded, because if it is, the sharing ratio is probably low and Flyweight may be the wrong pattern entirely.
"Where exactly is the memory saving, in bytes?"
The intrinsic payload is stored once per unique combo instead of once per instance. For a million characters with maybe 128 unique glyph combos at 200 bytes each, that is ~25KB of shared glyphs plus a million small position records (a few ints and a reference each), versus ~200MB of inlined duplication. The flyweights carry the heavy bytes once, the extrinsic records stay cheap.
"Give me a built-in flyweight from the standard library."
Integer.valueOf caches -128 to 127 and returns shared instances, so Integer.valueOf(100) == Integer.valueOf(100) is true while 1000 is not. Boolean.TRUE and interned String literals are the same idea. The Integer cache upper bound is even tunable with java.lang.Integer.IntegerCache.high.

What you can now explain in the room

  • How to split any object into intrinsic (shared, immutable) and extrinsic (per-use, passed in) state.
  • Why the factory's cache is the heart of the pattern, and how == proves sharing.
  • Why flyweights must be immutable, and the exact bug a single setter introduces.
  • When Flyweight pays off (large N, heavy shared state) and when it is premature optimization.
  • How an unbounded key space turns the pool into a leak, and how LRU or WeakReference fixes it.
  • The one-line difference between Flyweight (sharing across space) and an object pool (reuse over time).

Related: pair this with the Factory pattern (the pool lives inside one) and compare it against Prototype and the Object Pool pattern in your LLD prep. It also pairs naturally with Composite in editor and scene-graph problems.

Get a prep plan built around your Flyweight Pattern interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Flyweight Pattern prep plan is on the way.