Flyweight
Part of the free Low-Level Design course on LogicWiz, in the chapter: Structural patterns. The class-diagram exercises run in the app.
Flyweight: share the icon, not the rider
"The map service ran out of memory with 20,000 riders online. Every marker carries its own copy of the same three icons."
Priya, reading an out-of-memory alert
The problem: 20,000 markers, 20,000 icons
SwiftKart's operations map shows every online rider as a marker. A big city has about 20,000 riders online at lunchtime. A marker has a position, a vehicle (bike, scooter or electric, written "ev"), a colour and an icon. The icon is a small image of 4,096 bytes, which is 4 KB. Arjun's marker decodes its own icon in the constructor.
public class RiderMarker {
static int iconsDecoded = 0; // counts the icon arrays created
private final double lat;
private final double lng;
private final String vehicle;
private final byte[] iconPixels;
private final int colour;
public RiderMarker(double lat, double lng, String vehicle) {
this.lat = lat;
this.lng = lng;
this.vehicle = vehicle;
this.iconPixels = decodeIcon(vehicle); // every marker decodes its own icon
this.colour = vehicle.equals("ev") ? 0x2E7D32 : 0x1565C0;
}
private static byte[] decodeIcon(String vehicle) {
iconsDecoded++;
return new byte[4096]; // the real code decodes an image; the size is what matters
}
}
Priya ran the map with 20,000 riders, in three vehicle types, and counted:
// markers: 20000
// icon arrays created: 20000
// icon bytes: 81920000 (20,000 x 4,096)
Only three different icons exist, so 19,997 of those arrays are copies. When Priya limited the program to 64 MB of memory with -Xmx64m, it stopped with OutOfMemoryError: Java heap space. A heap is the memory area where Java keeps objects, and -Xmx sets its maximum size.
Arjun said, "Java has a garbage collector. Memory is not my job." Priya answered, "The collector frees objects that nothing points to. Here every marker points to its own icon, so nothing is garbage."
The idea: one library book, many bookmarks
Think of a library with one copy of a reference book and 20,000 readers. The library does not print a book for each reader. Every reader uses the same text, and each keeps a bookmark with their own page. The text is the same for everyone. The bookmark is different for each reader.
Divide the data of the marker in the same way:
- Intrinsic state is the data that is the same for many objects and does not depend on how an object is used. It is stored inside the shared object. Here: the vehicle, the icon pixels and the colour.
- Extrinsic state is the data that differs for each use. It stays outside the shared object and is passed in when needed. Here: the latitude and longitude.
To find the intrinsic state, ask: if I made 20,000 of these, which fields would hold identical values?
The flyweight pattern shares the intrinsic state. You make a small number of immutable objects, one for each distinct intrinsic value. A factory hands them out. Many markers hold a reference to the same icon. The name comes from boxing, where flyweight is the lightest weight class.
The library picture stops in one place. A library lends a book to one reader at a time. A flyweight is used by every marker at the same moment. That is only safe because nobody can write in it.
classDiagram
class RiderIcon {
-String vehicle
-byte[] pixels
+render(lat, lng) String
}
class RiderIconFactory {
-Map cache
+get(vehicle) RiderIcon
}
class RiderMarker {
-double lat
-double lng
}
RiderIconFactory --> RiderIcon : keeps one per vehicle
RiderMarker --> RiderIcon : shares
MapScreen ..> RiderIconFactory : asks for icons
MapScreen ..> RiderMarker : creates
The roles:
- Flyweight:
RiderIcon. Immutable and shared. - Flyweight factory:
RiderIconFactory. It makes an icon the first time and reuses it after that. - Context:
RiderMarker. It holds the extrinsic state and a reference to the flyweight. - Client:
MapScreen, which creates markers.
Build it step by step
Step 1: the flyweight. Every field is final, and nothing outside can change the pixels.
public final class RiderIcon {
private final String vehicle;
private final byte[] pixels; // private, and never changed after construction
private final int colour;
RiderIcon(String vehicle) { // package-private: only the factory creates icons
this.vehicle = vehicle;
this.pixels = new byte[4096];
this.colour = vehicle.equals("ev") ? 0x2E7D32 : 0x1565C0;
}
public String render(double lat, double lng) { // extrinsic state arrives as arguments
return vehicle + " icon at (" + lat + ", " + lng + ")";
}
}
The class never returns pixels, because a caller could change the array. render takes the position as arguments. That is how extrinsic state reaches the shared object.
Step 2: the factory. It keeps one icon for each vehicle.
public class RiderIconFactory {
private final Map<String, RiderIcon> cache = new ConcurrentHashMap<>();
public RiderIcon get(String vehicle) {
return cache.computeIfAbsent(vehicle, RiderIcon::new); // build once, then reuse
}
}
computeIfAbsent creates the icon only if the key is missing. Many requests may call get at once, so the map is a ConcurrentHashMap, a map that is safe when several threads use it.
Step 3: the marker keeps only what is its own.
public class RiderMarker {
private final double lat; // extrinsic: different for every rider
private final double lng;
private final RiderIcon icon; // intrinsic: shared by every rider on this vehicle
public RiderMarker(double lat, double lng, RiderIcon icon) {
this.lat = lat;
this.lng = lng;
this.icon = icon;
}
public String render() { return icon.render(lat, lng); }
}
The same 20,000 riders now print this:
// markers: 20000
// icon objects created: 3
// icon bytes: 12288
// same icon object for rider 0 and rider 3? true
Follow one call. map[0].render() asks the marker, and the marker passes its own latitude and longitude to the shared icon. The icon adds its vehicle and returns "bike icon at (12.9, 77.5)". Each marker is small now: an object header of about 12 bytes, two doubles and one reference, about 32 bytes in all. For 20,000 markers that is about 640 KB.
The counts are exact: 3 icons instead of 20,000. Memory is harder to measure. We called System.gc(), then read Runtime.totalMemory() - freeMemory() before and after building the map, on JDK 24. The old design held about 84 MB, and the new one about 0.8 MB. Repeated runs gave 83.5 to 84.3 MB and 0.78 MB. The method is approximate. The collector decides when memory is freed, and other JVMs lay objects out differently. Check it by arithmetic: 20,000 icons of about 4,112 bytes each (4,096 plus a 16-byte array header) is about 82 MB. The 640 KB of markers, the array of references and the three icons add up to the 0.8 MB measured. Trust the counts, and treat the megabytes as rough.
Step 4: why the shared object must be immutable. Suppose Arjun adds a setter so the map can highlight one rider in red.
MutableIcon forRider1 = cache.get("bike");
MutableIcon forRider2 = cache.get("bike"); // the same object
forRider1.setColour(0xC62828); // highlight rider 1
// rider 2's colour: c62828 -> every bike on the map turned red
One change reached all bikes, because there is only one icon. A highlight is extrinsic state. Pass it into render as an argument, or keep it in the marker.
You already use flyweights in Java
Two classes you have used since your first week share objects this way:
Integer a = 127, b = 127, c = 128, d = 128;
System.out.println(a == b); // true: both are the one cached Integer
System.out.println(c == d); // false: 128 is outside the cache
System.out.println(c.equals(d)); // true
String s1 = "milk", s2 = "milk", s3 = new String("milk");
System.out.println(s1 == s2); // true: both literals are one pooled String
System.out.println(s1 == s3); // false: new String always makes a fresh object
System.out.println(s1 == s3.intern()); // true: intern() returns the pooled one
Integer.valueOf, which boxing calls, is a flyweight factory. By default it caches values from -128 to 127, and a setting can raise the top. Java keeps one String object for each literal in a pool, and intern() looks one up. Both are safe because Integer and String cannot change. Boolean.valueOf and Character.valueOf also keep a small cache. Notice what these have in common with the rider icon: a factory method hands the object out instead of a constructor, and the shared objects can never change.
⚠️ Warning: Compare
IntegerandStringvalues withequals, not==. The==test checks whether two names point to one object, and the cache makes the answer depend on the value.
When to use it, when not to, and what it costs
Use a flyweight when:
- there are very many objects, tens of thousands or more;
- most of their state is identical across many of them;
- the shared state can be immutable;
- you measured memory and it is a real problem.
SwiftKart has other cases. A city map is 1,000 by 1,000 cells, which is 1,000,000 cells. If each cell keeps a reference to one of about 10 tile objects (road, water, park), there are 10 textures, not a million. A product has a category with a name and a tax rate. With 100,000 products and about 40 categories, each product points to one Category object. That is ordinary modelling, and you need the name "flyweight" only when saving memory is the reason.
Do not use it when:
- there are few objects. 150 riders with their own icons use about 600 KB, and the extra code costs more than that;
- most state is unique to each object, so there is little to share;
- the shared part must change for one user. Make it extrinsic or do not share it;
- nobody has measured a memory problem. Java objects are small, and a premature flyweight spreads complexity. Priya's rule: measure before you share.
Flyweight trades simplicity and some processing time for memory. Every call must carry the extrinsic values, and the code that computes them runs each time. The costs: you split every object's state in two, extrinsic values travel as arguments, and a factory must exist. The cache lives as long as the factory. Its keys must be few, such as three vehicle types. If the key were the rider id, the cache would grow without limit.
Compare the look-alikes:
- Singleton: exactly one object of a class. A flyweight factory holds a few, one for each distinct value.
- Object pool (such as database connections): one user borrows an object, then returns it, and the object may change. Flyweights are never borrowed, never returned and never change.
- Prototype: makes copies on purpose. A flyweight avoids making copies.
In interviews, flyweights appear as the characters in a text editor, where each letter shape is shared and the position is extrinsic. They also appear as trees, bullets or particles in a game. Machine-coding rounds rarely ask for it by name. Mention it when an interviewer asks how to hold a million similar objects.
Try it: draw the shared icon
The starter has a marker that holds its own icon pixels. Move the shared state into an icon class, add a factory that hands out one icon for each vehicle, and keep only the position in the marker.
📌 Summary: A flyweight shares the identical, immutable state of many objects, and passes the state that differs as arguments. A factory hands out the shared objects. Measure first, and skip it for few objects.
What Arjun learns next
These seven patterns arrange objects: fitting one to another, putting one door in front of many, wrapping, nesting, guarding, splitting two dimensions and sharing. The next chapter is about behaviour. Ops keeps changing the delivery-fee rules, and Arjun learns to swap a rule while the app runs: the strategy pattern.
Check your understanding
- In SwiftKart's rider marker, which data belongs in the shared flyweight object?
- A developer adds `setColour` to the shared bike icon to highlight one rider. Now every bike on the map turns red. What is the cause and the fix?
- In Java, `Integer a = 1000, b = 1000;` and then `a == b` is false, but the same test with 100 is true. Why?
- An admin dashboard shows 150 riders, each with its own 4 KB icon. A teammate wants to add a flyweight factory. What is the best advice?