Flyweight Pattern
Learn how to share data across thousands of near-identical objects instead of duplicating it, using a multiplayer shooter's bullets as the running example.
Learning objectives
- Explain the difference between intrinsic and extrinsic state
- Estimate memory cost per object and decide whether Flyweight is actually worth applying
- Implement a flyweight factory that guarantees a single shared instance per unique intrinsic state
- Recognize Flyweight in real Java APIs such as Integer caching and String interning
◆ Story
A printing press does not carve a fresh metal letter "A" every time an A appears on a page. It keeps one physical stamp for A and presses it wherever an A is needed, however many thousand times that turns out to be. The stamp itself is separate from the many places it gets used.
That separation — one shared, reusable "thing" versus many independent "uses" of that thing — is the entire idea behind the Flyweight pattern. It shows up in software whenever a program needs to create so many objects that simply storing all of them, each with its own private copy of every field, becomes the actual bottleneck.
Think about a multiplayer shooting game. Ten players fire roughly ten thousand bullets each over an hour of play — about one hundred thousand bullet objects in total. Every bullet needs a position on screen, a velocity, a color, and a small image to render. If each of those hundred thousand bullets stores its own independent copy of everything, including the image, the game is going to burn through memory fast, even though the vast majority of red bullets look byte-for-byte identical to every other red bullet.
Flyweight is not a general-purpose pattern you reach for by default. It solves one specific problem: an enormous number of objects, most of which share large chunks of identical data. When that specific shape appears, splitting an object's state into a shared part and a per-instance part can cut memory use by orders of magnitude. When it does not appear, the pattern is pure overhead — an extra layer of indirection for no real benefit.
This chapter builds the bullet example from the ground up: first the obvious, naive version, then the actual memory math showing why it is a real problem, then the fix, then the math redone to prove the fix worked.
◆ The problem
Before writing a single line of the fix, it is worth doing the actual memory arithmetic — this pattern only earns its complexity when a real number justifies it, not on a hunch.
A bullet needs an x and y position (an int each, 4 bytes apiece), a color (a short string, call it 10 bytes), and a small 20x20 pixel image (20 x 20 pixels x 3 color channels x 4 bytes per int, which comes out to roughly 4,800 bytes). Add it up and one bullet costs approximately 5,000 bytes — and almost all of that is the image.
Now apply the actual requirement: a multiplayer shooter where 10 players each fire about 10,000 bullets over an hour, for roughly 100,000 bullet objects total. The natural way to model this is one Bullet class holding every property directly.
Run the code below and it works fine — every bullet prints its color, position, and velocity correctly. The bug is not a functional bug. It is a hidden cost that only becomes visible once you multiply by the real object count: at about 5,000 bytes per bullet and 100,000 bullets, total memory for bullets alone comes out to roughly 500,000,000 bytes, or about 500 MB, in a single hour of one multiplayer match. Every one of those bullets is storing its own separate copy of data — especially the image — that is identical, byte for byte, to thousands of other bullets' copies of the exact same data.
▲ Common mistake
It is tempting to look at the code below and see nothing wrong with it, because nothing is functionally broken. The mistake is invisible until you do the multiplication. Always work out the real memory cost per object times the real object count before deciding a design is fine.
💻 Code example
class Bullet { private String color; // identical for every bullet of the same color -- but stored separately, every time private int x, y; private int velocity; Bullet(String color, int x, int y, int velocity) { this.color = color; this.x = x; this.y = y; this.velocity = velocity; } void display() { System.out.println(color + " bullet at (" + x + "," + y + ") velocity " + velocity); } } // firing bullets -- five red, five green, each a fully separate object for (int i = 0; i < 5; i++) { new Bullet("Red", i * 10, i * 12, 5); } for (int i = 0; i < 5; i++) { new Bullet("Green", i * 10, i * 12, 5); } // every "Red" bullet stores its own independent copy of the string "Red", and (in a real game) its own copy of the bullet image
Look closely at what actually varies between bullets, and what does not.
- Some properties are genuinely identical across many bullets. Every red bullet has the exact same color, and in a fuller version, the exact same image. These are called intrinsic properties: shared, reusable, and safe to store exactly once.
- Some properties are genuinely unique to each individual bullet. Its x and y position, its velocity. These are called extrinsic properties: they can never be shared, because they differ for every single instance.
- So intrinsic properties should be pulled out into their own shared object, and every bullet that needs "red" should hold a reference to that one shared object, instead of its own independent copy.
This split — intrinsic state shared and stored once, extrinsic state kept per instance — is the Flyweight pattern. It dramatically reduces memory whenever many instances share the same intrinsic data.
The implementation has two pieces. First, a flyweight class that holds only the intrinsic, shareable state — nothing about position or velocity belongs here at all. Second, a factory that guarantees only one flyweight instance ever gets constructed per unique combination of intrinsic properties. Without that factory, nothing stops the same BulletType("Red") from being constructed a hundred thousand times, which would defeat the entire point.
The factory's job is the same underlying question a Singleton answers — "who is responsible for making sure we don't create a duplicate?" — just generalized from one single instance total to one instance per unique key. It keeps a cache, checks it before constructing anything, and returns the same shared object every time the same key is requested.
Once the factory exists, Bullet itself changes to match: instead of storing color directly, it stores a reference to a shared BulletType, obtained by asking the factory for it.
💻 Code example
class BulletType { // the FLYWEIGHT -- this is what gets shared across thousands of bullets private final String color; // no x, no y, no velocity -- genuinely per-bullet data has no place here public BulletType(String color) { this.color = color; System.out.println("Creating bullet type with color: " + color); // only ever printed once per unique color } } class BulletTypeFactory { private static final Map<String, BulletType> bulletTypes = new HashMap<>(); // static: ONE cache, shared across the whole application public static BulletType getBulletType(String color) { if (!bulletTypes.containsKey(color)) { // has this exact color been requested before? bulletTypes.put(color, new BulletType(color)); // no -- construct it ONCE, and remember it } return bulletTypes.get(color); // yes, or just constructed -- either way, return the SAME shared instance } } class Bullet { private BulletType type; // intrinsic state -- a shared reference, not a duplicated copy private int x, y; // extrinsic state -- genuinely unique to THIS bullet private int velocity; Bullet(String color, int x, int y, int velocity) { this.type = BulletTypeFactory.getBulletType(color); // reuses an existing BulletType if one already exists for this color this.x = x; this.y = y; this.velocity = velocity; } }
The client code that fires bullets does not change at all. It calls new Bullet("Red", ...) exactly the way it did before the refactor — the sharing happens entirely inside the constructor, invisible to whoever is creating bullets. That is worth noticing: a well-applied Flyweight changes how data is stored internally without forcing every caller to change how they use the class.
Running the same loop that fires five red bullets and five green bullets now prints "Creating bullet type with color: Red" exactly once, and "Creating bullet type with color: Green" exactly once — not five times each. Every one of the five red Bullet instances shares the same single BulletType object in memory; only their x, y, and velocity differ.
Now redo the memory math from before, with the flyweight in place. Each Bullet stores only a reference to its BulletType (a handful of bytes, not the full ~5,000) plus its genuinely unique x, y, and velocity (about 8 to 12 bytes total). The ~5,000-byte cost is now paid once per unique color, not once per bullet.
| Without Flyweight | With Flyweight | |
|---|---|---|
| Per-bullet cost | ~5,000 bytes (color + image duplicated every time) | ~8 bytes (just x, y -- a reference is nearly free) |
| Shared cost | -- | ~5,000 bytes x 2 colors = 10,000 bytes, paid once |
| Total (100,000 bullets) | ~500,000,000 bytes, about 500 MB | ~800,000 + 10,000 bytes, about 0.8 MB |
That is roughly a 600x reduction in memory — not a rounding difference, but the entire reason this pattern exists. Gameplay logic and rendering are completely unaffected; only how the underlying data is stored changed.
💻 Code example
public class Game { public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Bullet("Red", i * 10, i * 12, 5); // same call signature as before -- nothing about USING Bullet changed } for (int i = 0; i < 5; i++) { new Bullet("Green", i * 10, i * 12, 5); } // output: "Creating bullet type with color: Red" printed ONCE, then "...Green" printed ONCE -- not five times each } }
Flyweight is a genuine, worthwhile optimization for exactly one shape of problem: enormous object counts with substantial shared state. It is also arguably the most niche pattern a working developer will run into. Applying it to a game with a handful of bullets on screen at once, or to data that is mostly unique per instance anyway, adds real complexity — the split between intrinsic and extrinsic state, a factory managing a cache — for a memory saving that was never actually needed. Confirm there is a genuine, measured memory problem, the way the earlier math confirmed one here, before reaching for this pattern.
▲ Edge case
A shared flyweight must be immutable. BulletType's color field is never modified after construction, and it cannot be, because one BulletType instance is now shared by potentially thousands of bullets. If any code mutated a shared BulletType's color, every bullet referencing that instance would change color simultaneously and silently — an extremely hard bug to trace, since nothing about the call site that triggered the mutation looks unusual. Shared state has to be treated as read-only, or the sharing itself becomes the bug.
▲ Edge case
The factory's cache never shrinks. BulletTypeFactory's HashMap never removes an entry once added, which is fine when the number of distinct intrinsic combinations is genuinely small and bounded — a handful of bullet colors. But it would itself become a memory leak if the set of unique combinations were unbounded or unpredictable, for example if color were an arbitrary user-supplied string instead of one of a few fixed values. Flyweight assumes a small, bounded set of shareable variations; if that assumption does not hold, the cache needs its own eviction strategy.
◆ Under the hood
Flyweight shows up in a handful of very recognizable places once you know the shape to look for.
Game engines rendering thousands of visually identical trees, particles, or bullets — this chapter's exact example — from one shared mesh or texture, with each on-screen instance only storing its own unique position and rotation. Rendering ten thousand trees from ten thousand fully independent 3D models would be a nonstarter; sharing one model and varying only position and orientation per instance is what makes it feasible at all.
Map applications such as Google Maps reuse a single "hotel" icon or "gas station" icon across every matching location on the map, rather than loading a separate copy of the icon image for each individual pin. Thousands of pins, one shared icon asset.
Java's own standard library uses this exact idea. Integer.valueOf() — and autoboxing, which calls it under the hood — caches Integer objects for values from -128 to 127, so that Integer a = 100; Integer b = 100; can end up pointing at the very same object rather than two separate ones. String interning works the same way: string literals are stored once in a shared pool, and identical literals across a program reuse that one shared instance instead of allocating a new String object every time. Character.valueOf() caches values for the same reason. In every one of these cases, the JVM is making the same bet this chapter's bullet example makes: an enormous number of small, frequently identical objects is worth the complexity of a shared cache.
Q: What is the difference between intrinsic and extrinsic state in the Flyweight pattern? A: Intrinsic state is shared and identical across many instances, such as a bullet's color — it gets stored once and reused. Extrinsic state is genuinely unique per instance, such as a bullet's x and y position, and it stays with each individual usage.
Q: What role does a flyweight factory play, and why can't the object just construct its shared state itself? A: The factory guarantees only one flyweight instance ever exists per unique key (like a color), by checking and updating a shared cache. If the object constructed its shared state directly, every instance would get its own separate copy, defeating the entire purpose of the pattern.
Q: Why must a shared flyweight object be immutable? A: Because one instance is referenced by many objects simultaneously. Mutating it would silently change every object that references it at once, which is an extremely hard bug to trace back to its cause.
Q: When is Flyweight actually worth reaching for? A: Only when there is a genuine, measured memory problem coming from an enormous object count combined with substantial shared state. It is one of the most niche patterns in common use, and easy to over-apply to problems that do not actually need it.
Q: In the bullet example, roughly how much memory does the flyweight version save? A: About 500 MB drops to under 1 MB for 100,000 bullets with 2 distinct colors, because the roughly 5,000-byte cost per color is paid once instead of once per bullet.
Want a visual for this concept?
Generate a diagram tailored to “Flyweight Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →