Free Interactive Course · Design Patterns

Flyweight Design Pattern

Share what's identical across a million objects, and keep only what differs — memory stops scaling with object count.

Structural Patternsharda.k.a. Cachea.k.a. Shared Object
ShareXLinkedIn
In one sentence

Use sharing to support very large numbers of fine-grained objects efficiently, by separating the state that can be shared from the state that cannot.

01

The problem Flyweight solves

The problem

Your game renders a forest: one million trees. Each Tree object holds its position, its scale, a slight rotation — and its mesh, its bark texture and its leaf texture, about 2 MB of geometry and pixels.

One million times 2 MB is 2 TB. The level doesn't load. It isn't close to loading.

And here is the maddening part: there are only six species of tree in the whole forest. Every one of those million objects is holding its own private copy of one of six identical meshes. The data that actually differs between two oaks is three floats — a position, a scale, an angle — roughly 24 bytes. You are paying two megabytes to store twenty-four bytes of new information.

02

How the Flyweight pattern works

Split the object's state in two, along the line of "could two of these ever differ?":

  1. Intrinsic state is what's identical between instances and never changes: the mesh, the textures, the species name. Move it into a shared, immutable flyweight object.
  2. Extrinsic state is what differs per instance: position, scale, rotation. It stays with the caller, or is passed in as an argument.
  3. A factory hands out flyweights by key, creating one the first time and returning that same instance forever after. Callers never use new.
  4. A million trees now hold a pointer to one of six shared objects. Memory stops growing with the number of objects and starts growing with the number of distinct kinds.
Immutability isn't a style preference here — it's the load-bearing requirement. The moment one caller can mutate a shared flyweight, it mutates it for the other 999,999, and you get a bug that appears at a distance, under load, in a way that is almost impossible to reproduce. Make the flyweight's fields final, private and copy-free; if you ever need a mutable variant, that state is by definition extrinsic and belongs on the caller.
EXTRINSIC · 1,000,000INTRINSIC · 6Tree(x:12, y:88, s:1.1)Tree(x:41, y:12, s:0.9)Tree(x:77, y:53, s:1.4)… 999,997 more · 24 bytes eachTreeTypeFactoryof(species) → sharedoak · mesh + 2 texpine · mesh + 2 texbirch · mesh + 2 tex2 TB → 24 MB + 12 MBmemory now scales with the number of KINDS, not the number of objects
Participants. The Flyweight (TreeType) holds the immutable intrinsic state. The Factory owns the pool and guarantees that one key returns one instance. The Client keeps the extrinsic state and a reference to a flyweight. Note the direction: flyweights never know their clients, which is what allows one to be shared by a million of them.
03

See it: memory that stops growing

Ask for trees. Ask for the same species several times. Watch the object count and the memory — then turn the pattern off and ask for exactly the same things again.

▶ Try it — plant a forest

An interactive object pool: requesting oak, pine, birch and other species returns one shared TreeType per species — so asking twenty times still allocates six objects. Turn the pattern off and every request allocates its own 2 MB copy of an identical mesh and texture.

Click oak five times with the pattern on: five trees, one object, 2 MB. Turn it off and click oak five times again: five objects, 10 MB, holding five byte-for-byte identical meshes. Scale that to a million and the difference is the level loading or not — and notice the pattern didn't reduce the number of trees, only the number of distinct objects.
04

Flyweight pattern code examples

An immutable shared type, a factory that guarantees one per key, and the extrinsic state left with the caller.

/** The flyweight: intrinsic state only, deeply immutable, shared by everyone. */
public record TreeType(String species, Mesh mesh, Texture bark, Texture leaf) {

    private static final Map<String, TreeType> POOL = new ConcurrentHashMap<>();

    /** The factory. Callers never see `new`. */
    public static TreeType of(String species) {
        // computeIfAbsent is atomic — two threads planting the first oak at the
        // same moment still end up with exactly one TreeType.
        return POOL.computeIfAbsent(species, TreeType::load);
    }

    private static TreeType load(String species) {
        return new TreeType(species, Meshes.load(species), Textures.bark(species), Textures.leaf(species));
    }

    /** Extrinsic state arrives as arguments — it is never stored here. */
    public void draw(Canvas canvas, int x, int y, float scale) {
        canvas.render(mesh, bark, leaf, x, y, scale);
    }
}

/** The client keeps only what actually differs: 24 bytes, not 2 MB. */
public record Tree(int x, int y, float scale, TreeType type) {
    public void draw(Canvas canvas) { type.draw(canvas, x, y, scale); }
}

// The JDK ships flyweights you use without noticing:
//   Integer.valueOf(127) == Integer.valueOf(127)   // true  — cached -128..127
//   Integer.valueOf(128) == Integer.valueOf(128)   // false — outside the cache
// String literals are interned into a shared pool for exactly the same reason.
// That first pair of lines is also the classic Java interview question.
Read across the tabs: most of these languages already ship flyweights you use without naming them — Java's Integer.valueOf cache and string literal pool, CPython's small-int and interned-string tables, .NET's string interning, JavaScript's Symbol.for registry, Go's time.LoadLocation. Two warnings recur across every tab, and they're the ones that bite in production: an unbounded pool is a memory leak with a friendly name, and sharing costs a pointer dereference — for small payloads a packed array of plain structs can beat a flyweight outright, so measure before you assume.
05

How to implement Flyweight

  1. Measure first. Flyweight is an optimisation, and it costs you indirection and a factory — apply it when a profiler says object count is the problem, not on suspicion.
  2. Split the fields: anything that could differ between two instances is extrinsic and must leave the shared object.
  3. Make the flyweight deeply immutable — final fields, no mutable collections, no arrays handed out by reference.
  4. Write a factory keyed on the intrinsic state, and make callers use it instead of a constructor. Hide or delete the public constructor.
  5. Make the factory thread-safe with a concurrent map or a once-primitive, so the first concurrent request doesn't create two.
  6. Bound the pool. A cache keyed on user input with no eviction is a leak. Fixed key space, or an LRU with a real limit.
  7. Pass extrinsic state as arguments, and keep the caller's per-instance record as small and as contiguous in memory as your language allows.
06

When to use Flyweight — and when not to

Use it when an application creates a very large number of similar objects, when storage cost is genuinely a problem, when most of each object's state can be made extrinsic, and when the number of distinct objects after sharing is far smaller than the number of instances. If those four aren't all true, you are adding a factory and an indirection for nothing.

Where it goes wrong

A mutable flyweight. One caller sets a field and every other user of that instance changes with it, at a distance, under load. This is the pattern's signature bug and it is brutal to diagnose. Immutability is not optional.

An unbounded pool. "Cache the parsed value by string key" is a flyweight factory, and if the keys come from user input it is a memory leak that grows exactly as fast as your traffic. Bound it or key it on a closed set.

Slower, not faster. Sharing replaces inline data with a pointer, and a pointer is a potential cache miss on every access. For small intrinsic state, a packed array of plain values regularly beats a flyweight. Benchmark rather than assume.

Identity comparisons that used to work. After sharing, a == b may be true for objects that were logically distinct, and code that used object identity as a key silently merges them.

Premature application. Flyweight makes construction indirect and debugging harder for every reader of the code. Ten thousand objects do not need it; ten million might.

You want to…UseBecause
Share identical immutable state across many objectsFlyweightMemory scales with distinct kinds, not instance count.
Guarantee exactly one instance of a classSingletonSingleton is a flyweight pool with exactly one key — and a global.
Copy an expensive object instead of rebuilding itPrototypePrototype makes distinct copies; Flyweight avoids copies entirely.
Delay creating an expensive object until neededProxyProxy defers; Flyweight shares. They combine well.
Hide which concrete class the caller getsFactory MethodEvery Flyweight needs a factory; not every factory shares.
07

Quick check

🧠 Quick check
A colleague adds a timesDrawn counter to TreeType and increments it in draw(). What happens?

In the wild

JavaInteger.valueOf() caches −128 to 127, which is why Integer.valueOf(127) == Integer.valueOf(127) is true and 128 is not. String literals are interned into a shared pool for the same reason.
PythonCPython pre-allocates small integers and interns string literals, so 256 is 256 is true and 257 is 257 is not; sys.intern() forces it explicitly.
C#string.Intern() and the runtime's automatic literal interning; ArrayPool<T> for the related but distinct recycling case.
JavaScriptSymbol.for('key') — a global registry where the same key always yields the identical symbol, in every module of the realm.
Gotime.LoadLocation("Europe/London") returns the same shared *time.Location every time. Not to be confused with sync.Pool, which recycles mutable objects.
C++Qt's implicit sharing (copy-on-write QString, QPixmap), and std::string_view, which shares characters instead of copying them.

Frequently asked questions

What is the difference between intrinsic and extrinsic state?
Intrinsic state is identical for every object sharing the flyweight and never changes — a tree species' mesh and textures, a character glyph's outline, a log event type's schema. Extrinsic state is what makes one instance different from another: position, scale, timestamp. Intrinsic state lives inside the shared object; extrinsic state stays with the client and is passed in as arguments. The test is simple: if two instances could ever disagree about a field, it's extrinsic.
What is the difference between Flyweight and an object pool?
They share the word "pool" and almost nothing else. A flyweight is permanently shared, immutable, and used by many clients at the same time. An object pool (Go's sync.Pool, a JDBC connection pool) recycles mutable objects one borrower at a time to avoid allocation or setup cost, and expects them back. Flyweight optimises memory through sharing; object pooling optimises allocation through reuse. Mutating a pooled object is the point; mutating a flyweight is the bug.
Is String interning an example of the Flyweight pattern?
Yes, and it's the one nearly every developer has already used. Java, C# and Python all keep a pool of string values so identical literals share one object — the intrinsic state is the character data, and there is no extrinsic state at all. Java's Integer cache for −128 to 127 is the same idea and produces the famous interview question where == is true for 127 and false for 128.
Does Flyweight always improve performance?
No, and it's worth being blunt about it. You trade inline data for a pointer, and following that pointer can cost a cache miss on every access — so for small intrinsic state a packed array of plain values often beats a flyweight comfortably. You also add a factory lookup on construction and make debugging harder. The pattern pays when the shared payload is large, the instance count is enormous, and the distinct-kind count is small. Profile before and after; if the numbers don't move, remove it.
How do I stop a flyweight pool leaking memory?
Bound it. A pool keyed on a closed set — six tree species, a fixed set of event types — can never grow and is safe forever. A pool keyed on anything user-supplied (a URL, a customer id, a parsed query) grows with traffic and is a leak with a helpful name. Use an LRU with a real limit, weak references where your language supports them, or don't cache that key space at all.

Finish the handbook, earn the certificate

All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.

See all 23 patterns →
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.