Up to this point, the structural patterns have organized pieces you can count on your fingers: an adapter, two hierarchies, a menu tree, a few layers of extras, a facade. Flyweight enters the stage when the pieces number in the tens of thousands and memory starts to complain. It is the most specialized pattern in the module —the one you will apply the fewest times— and at the same time one you use daily without knowing it, because Java itself ships with it. Its idea fits in one sentence: if ten thousand objects repeat the same data, let the ten thousand share a single copy of what repeats and let each carry only what is genuinely its own.
Contents
- The problem in PideYa: the real-time map
- Intrinsic state and extrinsic state
- Intent and structure of the pattern
- Complete Java implementation
- Flyweight in the JDK:
Integer.valueOfand the string pool - When it is NOT worth it
- Common mistakes, exercises, and conclusion
The problem in PideYa: the real-time map
PideYa's operations center shows a city map with all couriers and restaurants live: in Madrid at rush hour, around 4,000 couriers (on bike, motorbike, or car, each type with its icon and its color by status: available, delivering, on break) and 9,000 restaurants (icon per category: pizzeria, sushi, burgers...). Each marker refreshes several times per second. The first model, one by one:
// First attempt: every marker hauls EVERYTHING around. Do NOT imitate.
public class MapMarker {
// What changes per marker:
private String id; // "courier-8841"
private double latitude;
private double longitude;
// What repeats across thousands of identical markers:
private byte[] icon; // the icon PNG... 32 KB!
private String typeLabel; // "Courier on motorbike"
private Color color; // by status
private int widthPx, heightPx; // render size
}The math is devastating: 13,000 markers × ~32 KB of icon ≈ 400 MB in icons alone... when in reality there are only a few dozen distinct combinations of (icon, label, color, size): motorbike-available, motorbike-delivering, bike-available, pizzeria, sushi... Everything else is the same information duplicated thousands of times. In the customer app (modest phones) the map simply doesn't fit; in the operations panel, the garbage collector never catches a break.
The waste has a precise structure: each marker mixes two kinds of state with completely different lives. Separating them is the entire pattern.
Intrinsic state and extrinsic state
The central distinction of Flyweight, worth mastering before seeing a single line of code:
| Intrinsic state | Extrinsic state | |
|---|---|---|
| What it is | What the object is by its type: identical across all specimens of that marker kind | What the object is by its context: different in each appearance |
| On the map | Icon, type label, status color, size | Courier/restaurant id, latitude, longitude |
| Context-dependent? | No: "motorbike-available" is the same here and across town | Yes: each marker has its own |
| Mutable? | Never: it must be immutable to be shared | Freely: it changes with every GPS refresh |
| Where it lives | Inside the flyweight, one shared copy | Outside: the client supplies it on every call |
| How many there are | Dozens (type × status combinations) | Thousands (one per entity on the map) |
The pattern's recipe: extract the intrinsic state into shared, immutable objects (the flyweights), move the extrinsic state outside (the client passes it as parameters on every operation), and install a factory that guarantees each intrinsic combination is created exactly once.
Intent and structure of the pattern
Intent (GoF): use sharing to support large numbers of fine-grained objects efficiently.
classDiagram
class MarkerIcon {
<<flyweight>>
-icon: byte[]
-typeLabel: String
-color: Color
-widthPx: int
-heightPx: int
+draw(canvas: Canvas, latitude: double, longitude: double)
}
class IconFactory {
-cache: Map~IconKey, MarkerIcon~
+get(type: EntityType, status: Status) MarkerIcon
+cacheSize() int
}
class MapMarker {
-id: String
-latitude: double
-longitude: double
-icon: MarkerIcon
+draw(canvas: Canvas)
}
class OperationsPanel
IconFactory o-- MarkerIcon : caches and shares
MapMarker --> MarkerIcon : shared reference
OperationsPanel --> IconFactory : gets flyweights
OperationsPanel --> MapMarker : holds thousands
| GoF role | In PideYa |
|---|---|
| Flyweight (shared, immutable intrinsic state) | MarkerIcon |
| FlyweightFactory (creates or reuses, caches) | IconFactory |
| Client / context (holds the extrinsic state) | MapMarker (id, position) and the panel |
Two observations about the diagram:
- The flyweight's operation (
draw) receives the extrinsic state as parameters: the flyweight doesn't know where it is, it gets told on every call. That signature is the pattern's hallmark. - The context object (
MapMarker) is stripped to the minimum: an id, two doubles, and one reference to the shared flyweight. From 32 KB to a few dozen bytes per marker.
Complete Java implementation
The flyweight — deliberately immutable, the non-negotiable condition for sharing it (even across threads, for free):
public final class MarkerIcon {
private final byte[] icon; // the 32 KB, ONCE per combination
private final String typeLabel;
private final Color color;
private final int widthPx;
private final int heightPx;
MarkerIcon(byte[] icon, String typeLabel, Color color, int widthPx, int heightPx) {
this.icon = icon.clone(); // defensive copy: nobody mutates what is shared
this.typeLabel = typeLabel;
this.color = color;
this.widthPx = widthPx;
this.heightPx = heightPx;
}
/** The extrinsic state (position) arrives as parameters on EVERY call. */
public void draw(Canvas canvas, double latitude, double longitude) {
canvas.drawImage(icon, latitude, longitude, widthPx, heightPx, color);
}
public String getTypeLabel() { return typeLabel; }
}The flyweight factory — the guardian of the sharing; notice that the MarkerIcon constructor is package-private: only the factory creates flyweights:
public class IconFactory {
/** The key identifies the full intrinsic combination. */
private record IconKey(EntityType type, Status status) {}
private final Map<IconKey, MarkerIcon> cache = new ConcurrentHashMap<>();
private final ResourceLoader resources;
public IconFactory(ResourceLoader resources) { this.resources = resources; }
public MarkerIcon get(EntityType type, Status status) {
return cache.computeIfAbsent(new IconKey(type, status), key ->
new MarkerIcon(
resources.loadPng(key.type()), // the 32 KB, only the 1st time
key.type().label(),
key.status().color(),
48, 48));
}
public int cacheSize() { return cache.size(); }
}The context and the client — thousands of lightweight markers pointing at dozens of shared icons:
public class MapMarker {
private final String id;
private volatile double latitude, longitude; // extrinsic: changes with every GPS fix
private final MarkerIcon icon; // SHARED reference
public MapMarker(String id, double lat, double lon, MarkerIcon icon) {
this.id = id; this.latitude = lat; this.longitude = lon; this.icon = icon;
}
public void moveTo(double lat, double lon) { this.latitude = lat; this.longitude = lon; }
public void draw(Canvas canvas) {
icon.draw(canvas, latitude, longitude); // the context supplies the extrinsic part
}
}IconFactory factory = new IconFactory(resources);
List<MapMarker> markers = new ArrayList<>();
for (LiveCourier c : liveFleet()) { // 4,000 couriers
markers.add(new MapMarker(
c.getId(), c.getLatitude(), c.getLongitude(),
factory.get(c.getVehicleType().entityType(), c.getStatus())));
}
for (Restaurant rest : openRestaurants()) { // 9,000 restaurants
markers.add(new MapMarker(
rest.getId(), rest.getLatitude(), rest.getLongitude(),
factory.get(rest.getCategory().entityType(), Status.OPEN)));
}
markers.forEach(m -> m.draw(canvas));
System.out.println("Flyweights created: " + factory.cacheSize()); // ~25, not 13,000The math after the pattern: ~25 flyweights × 32 KB ≈ 0.8 MB of icons (before: 400 MB), plus 13,000 contexts of ~50 bytes. Two orders of magnitude, without touching the rendering. The factory —a first cousin of the ones from module 2, but with memory— is what turns good intentions into a guarantee: nobody can create a duplicate icon because nobody else can create icons.
Flyweight in the JDK: Integer.valueOf and the string pool
Java has practiced this pattern forever, in two places you have already used:
Integer.valueOf(int)(and autoboxing, which calls it under the hood) keeps a cache of the integers from −128 to 127:Integer.valueOf(42) == Integer.valueOf(42)istrue— same shared instance —, whilevalueOf(1000) == valueOf(1000)isfalse. It is anIconFactoryfor integers: the most frequent values, shared; the rest, created. (And the eternal moral:Integervalues are compared withequals, never with==, precisely because the sharing is an internal detail.)- The string pool: String literals are interned and shared (
"pizza" == "pizza"istrue;new String("pizza")creates another instance outside the pool, andintern()brings it back into the fold). Possible becauseStringis immutable — the same condition we demanded ofMarkerIcon. Massive sharing of fine-grained objects: textbook Flyweight.
In the same family: Boolean.valueOf, Character.valueOf (ASCII), enums (each constant is a single shared instance). When a static valueOf hands you back "the" object instead of "an" object, suspect a flyweight.
When it is NOT worth it
Flyweight has the highest entry threshold of any pattern in the module, and it deserves to be said bluntly. Do not apply it when:
- There is no crowd. With hundreds of objects —even a few thousand small ones— the JVM doesn't even flinch. The pattern starts paying off with tens of thousands of instances and heavy repeated state. Before applying it, measure (a profiler,
Runtime.totalMemory(), a heap dump): optimization without measurement is superstition, and this pattern is, above all, an optimization. - The state barely repeats. If every marker had a custom icon, there would be nothing to share: the factory's cache would grow to hold one flyweight per object — all of the cost, none of the rent.
- The "intrinsic" state mutates. If the icon's color must blink per individual marker, that datum was extrinsic no matter how type-like it looked. Sharing mutable state isn't Flyweight: it's a heisenbug factory across threads.
- The price in clarity doesn't pay. Splitting intrinsic/extrinsic complicates signatures (everything travels as parameters) and reasoning. For 95% of PideYa's code —orders, carts, notifiers— that price would buy nothing: the objects are counted one at a time. The trade-off scale of lesson 01-06, once again.
When yes, stated positively: crowds (>10⁴) of fine-grained objects, with a substantial part of their state repeated, immutable or immutable-izable, and measured memory pressure. Maps, editors (one flyweight per character/style, the original GoF example), games (particles, tiles), glyph and sprite caches.
Relationship to other patterns (mentions only): the IconFactory is a module 2 factory with a cache — and often a single instance (Singleton or injected); the shared leaves of a huge Composite can be flyweights; Prototype is in a way its mirror: cloning so everyone gets their own copy versus sharing so nobody does; and don't confuse the flyweight cache (shares immutable objects to save memory) with a caching Proxy (avoids expensive calls by remembering results), which we will see next.
Common Mistakes and Tips
- A mutable flyweight. The lethal mistake. Someone adds
setColor(...)toMarkerIcon"to highlight one courier"... and highlights the 800 that share the instance. Every flyweight:finalclass,finalfields, defensive copies, and no setters. The compiler is your guardian if you let it be. - Sneaking extrinsic state inside. "While we're at it, let the icon remember the last drawn position": goodbye sharing (each marker would need its own) or goodbye correctness (everyone trampling the same datum). The discipline of the intrinsic/extrinsic border is the entire pattern.
- A leaky factory. The cache retains flyweights forever; if the keys are unbounded (one flyweight per restaurant instead of per category?), the "memory optimization" becomes a memory leak. Keys of bounded, known cardinality; if they aren't, it wasn't intrinsic state.
- Bypassing the factory. A
new MarkerIcon(...)outside the factory silently breaks the uniqueness guarantee. Close it off by construction: a package-private constructor (as we did) or one nested inside the factory. - Applying it just in case. Designing the map "with flyweight from day one" when the pilot has 40 couriers is textbook premature optimization. First the simple model; the pattern, when the profiler asks for it (refactoring toward it is mechanical: extract the repeated fields, create the factory, substitute).
- Tip: expose
cacheSize()(or equivalent metrics) on the factory from day one. A number that should hover around 25 and reads 11,000 is the smoke detector for nearly all the mistakes above.
Exercises
Exercise 1: separating the grain
PideYa's order history keeps in memory, for live analytics, one object per order line of the day (about 2 million): dishName (String), unitPrice (BigDecimal), taxCategory (String), photoUrl (String), quantity (int), orderTime (LocalTime), orderId (long). Classify each field as intrinsic or extrinsic and sketch the resulting flyweight and its factory key.
Exercise 2: the highlight bug
Operations asks to highlight the selected courier's marker in yellow. A colleague implements it by adding setHighlighted(boolean) to MarkerIcon. Explain the exact bug the panel will show and give two solutions compatible with the pattern.
Exercise 3: flyweight or not?
Decide with criteria (crowd, repetition, immutability, measurement) whether you would apply Flyweight: (1) the 25 possible OrderState objects referenced by millions of orders; (2) the 300 dish photos, all different, shown in one restaurant's menu; (3) the DeliveryZone objects (a polygon of ~200 vertices) shared by the ~150 couriers of each zone.
Solutions
Solution 1: intrinsic (massively repeated, invariant per appearance): dishName, unitPrice, taxCategory, photoUrl — together they form a CatalogProduct flyweight; PideYa's catalog has ~50,000 distinct dishes versus 2M lines: 40:1 sharing. Extrinsic (owned by each line): quantity, orderTime, orderId. Result: record AnalysisLine(long orderId, int quantity, LocalTime time, CatalogProduct product), with a CatalogProductFactory caching by key dishId (or name+restaurant). An honest nuance: if a dish's price changes during the day, the price at order time is extrinsic (historical), not intrinsic — the border is set by semantics, not by data type.
Solution 2: the bug: calling icon.setHighlighted(true) on the "motorbike-delivering" flyweight makes every delivering motorbike courier turn yellow at once (they share the instance); on deselect, they all switch off. Solutions: (a) the highlight is extrinsic state: the panel stores selectedId and passes it at draw time (icon.draw(canvas, lat, lon, highlighted)), so the flyweight stays immutable; (b) treat "highlighted" as part of the intrinsic key: (type, status, highlighted) combinations in the factory — it doubles the combinations (still ~50) and the selected marker gets a different flyweight. (a) is preferable: the highlight is context of the panel session, not identity of the type.
Solution 3: (1) It already is a flyweight without the name: 25 immutable instances shared by millions — natural implementation: an enum OrderState, which is the idiomatic Java form of the pattern for closed catalogs. (2) No: 300 objects all different = nothing repeated to share; their problem (not loading 300 photos at once) is one of lazy loading, which is exactly the virtual proxy of the next lesson. (3) Yes, and almost without ceremony: heavy polygons (~200 vertices), 150:1 repetition, immutable geometry; it is enough for couriers to receive the reference to the shared DeliveryZone instead of copying it — flyweight with no elaborate factory, just the discipline of sharing. Note that (3) shows the pattern is sometimes just "stop copying".
Conclusion
Flyweight attacks multiplication: it separates intrinsic state (repeated, immutable) from extrinsic state (contextual, changing), shares the former through a factory with memory, and makes the latter travel as parameters. In PideYa it put the real-time map on a diet —from 400 MB to under one, with MarkerIcon and IconFactory— and along the way taught you to read Integer.valueOf and the string pool for what they are. And it was said plainly when not to use it: without a measured crowd, without repetition, and without immutability, this pattern only adds friction.
One last structural pattern remains, and it closes the circle of the wrappers: an object that passes itself off as another —same interface, like Decorator— not to add responsibilities, but to control access: postponing an expensive load until it is needed, checking permissions before letting a call through, remembering answers to avoid repeat trips. In PideYa, the dish photos (today all loaded at once) and the admin panel's sensitive operations are waiting for it. See you in Proxy.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
