Flyweight
Flyweight saves memory when many small objects repeat the same heavy data: the common part is stored once and shared, and each object keeps only what's its own. This page loads Pedal's 40,000 bikes in about 1.9 MB instead of 170 MB, and lets you decide which fields can safely be shared.
1. The pain
The fleet screen in Pedal's operations app loads all 40,000 bikes at once so staff can filter and sort them on a tablet. Each bike object carried everything about its model: name, top speed, the maintenance manual text and a photo thumbnail, about 4.2 KB, and then its own 48 bytes of id, station and battery.
There are only three models. The other 39,997 copies of the model data are identical, and they add up to about 170 MB, so older tablets ran out of memory before the list appeared. Worse, when the manual was updated, some bikes kept the old text.
2. The idea in one picture
Flyweight shares the common part of many fine-grained objects. Each object's state is split in two: intrinsic state, the same for many objects and never changed (a model's name, speed, manual), lives in one shared flyweight; extrinsic state, different for each object (id, station, battery), stays with the object or is passed in when needed. A factory makes sure there's only one flyweight per model.
Flyweights must be immutable. If one bike could change "its" model's top speed, it would change every bike of that model. Python's interned strings and Java's cached small Integer values are flyweights built into the languages.
3. The roles
| Role | Its job | In Pedal |
|---|---|---|
| Flyweight | Holds the shared, unchanging (intrinsic) state; takes per-object state as parameters. | BikeModel |
| Flyweight factory | Returns the existing flyweight for a key, or creates it once. | ModelRegistry.get(name, …) |
| Context | The many small objects; hold their own (extrinsic) state and a reference to a flyweight. | Bike: id, station, battery, model |
| Client | Creates contexts through the factory. | The fleet screen loading 40,000 bikes |
4. Before and after
A bike object both ways (Python sketch; the full program is in section 5).
Without the pattern
class Bike:
def __init__(self, bike_id, station, battery,
model_name, max_kmh, manual, photo):
self.bike_id = bike_id
self.station = station
self.battery = battery
self.model_name = model_name # copied
self.max_kmh = max_kmh # copied
self.manual = manual # 3 KB, copied
self.photo = photo # 1 KB, copiedWith it
class Bike:
def __init__(self, bike_id, model,
station, battery):
self.bike_id = bike_id
self.model = model # shared, read-only
self.station = station
self.battery = battery
model = registry.get("Pedal e-bike Mk2", 25, True)
bike = Bike("B-2", model, "S-12", 84)| The next change | Without the pattern | With it |
|---|---|---|
| Load 40,000 bikes | About 170 MB | About 1.9 MB |
| The e-bike manual is updated | 40,000 objects to update, or stale copies | One BikeModel replaced |
| Ask "which bikes are the same model?" | Compare field by field | Compare model references |
The saving grows with the number of objects that share each flyweight. With 40,000 bikes and 3 models, nearly all the memory was duplicates.
5. The code
A registry hands out one BikeModel per model name; 40,000 bikes point at three of them. The memory figures use the sizes declared at the top of the program.
from __future__ import annotations
MODEL_BYTES = 4_200 # name, specs, manual text, photo thumbnail: the same for every bike of a model
BIKE_BYTES = 48 # id, station, battery: different for every bike
class BikeModel:
"""The flyweight: shared, never changed after creation (intrinsic state)."""
def __init__(self, name: str, max_kmh: int, electric: bool) -> None:
self.name, self.max_kmh, self.electric = name, max_kmh, electric
def describe(self, bike_id: str, station: str, battery: int) -> str:
# per-bike (extrinsic) state is passed in, not stored here
charge = f", battery {battery}%" if self.electric else ""
return f"{bike_id}: {self.name}, max {self.max_kmh} km/h{charge}, at {station}"
class ModelRegistry:
"""The flyweight factory: hands out one shared object per model name."""
def __init__(self) -> None:
self.models: dict[str, BikeModel] = {}
def get(self, name: str, max_kmh: int, electric: bool) -> BikeModel:
if name not in self.models:
self.models[name] = BikeModel(name, max_kmh, electric)
return self.models[name]
class Bike:
"""The per-bike object: small, and points at its shared model."""
def __init__(self, bike_id: str, model: BikeModel, station: str, battery: int) -> None:
self.bike_id, self.model, self.station, self.battery = bike_id, model, station, battery
def describe(self) -> str:
return self.model.describe(self.bike_id, self.station, self.battery)
if __name__ == "__main__":
registry = ModelRegistry()
specs = [("Pedal classic", 20, False), ("Pedal e-bike Mk2", 25, True), ("Pedal cargo", 18, False)]
bikes = [Bike(f"B-{i + 1}", registry.get(*specs[i % 3]), "S-12", 84 if specs[i % 3][2] else 0) for i in range(40_000)]
print(f"bikes: {len(bikes)} | model objects: {len(registry.models)}")
print(f"B-1 and B-4 share one model object: {bikes[0].model is bikes[3].model}")
print(bikes[1].describe())
without = len(bikes) * (BIKE_BYTES + MODEL_BYTES)
with_sharing = len(bikes) * BIKE_BYTES + len(registry.models) * MODEL_BYTES
print(f"memory without sharing: {without:,} bytes")
print(f"memory with sharing: {with_sharing:,} bytes")
const MODEL_BYTES = 4_200; // name, specs, manual text, photo thumbnail: the same for every bike of a model
const BIKE_BYTES = 48; // id, station, battery: different for every bike
// The flyweight: shared, never changed after creation (intrinsic state).
class BikeModel {
constructor(readonly name: string, readonly maxKmh: number, readonly electric: boolean) {}
describe(bikeId: string, station: string, battery: number): string {
// per-bike (extrinsic) state is passed in, not stored here
const charge = this.electric ? `, battery ${battery}%` : "";
return `${bikeId}: ${this.name}, max ${this.maxKmh} km/h${charge}, at ${station}`;
}
}
// The flyweight factory: hands out one shared object per model name.
class ModelRegistry {
readonly models = new Map<string, BikeModel>();
get(name: string, maxKmh: number, electric: boolean): BikeModel {
if (!this.models.has(name)) this.models.set(name, new BikeModel(name, maxKmh, electric));
return this.models.get(name)!;
}
}
// The per-bike object: small, and points at its shared model.
class Bike {
constructor(readonly bikeId: string, readonly model: BikeModel, readonly station: string, readonly battery: number) {}
describe(): string { return this.model.describe(this.bikeId, this.station, this.battery); }
}
const registry = new ModelRegistry();
const specs: [string, number, boolean][] = [["Pedal classic", 20, false], ["Pedal e-bike Mk2", 25, true], ["Pedal cargo", 18, false]];
const bikes: Bike[] = [];
for (let i = 0; i < 40_000; i++) {
const [name, kmh, electric] = specs[i % 3];
bikes.push(new Bike(`B-${i + 1}`, registry.get(name, kmh, electric), "S-12", electric ? 84 : 0));
}
console.log(`bikes: ${bikes.length} | model objects: ${registry.models.size}`);
console.log(`B-1 and B-4 share one model object: ${bikes[0].model === bikes[3].model ? "True" : "False"}`);
console.log(bikes[1].describe());
const withoutSharing = bikes.length * (BIKE_BYTES + MODEL_BYTES);
const withSharing = bikes.length * BIKE_BYTES + registry.models.size * MODEL_BYTES;
console.log(`memory without sharing: ${withoutSharing.toLocaleString("en-US")} bytes`);
console.log(`memory with sharing: ${withSharing.toLocaleString("en-US")} bytes`);import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class Main {
static final int MODEL_BYTES = 4_200; // name, specs, manual text, photo thumbnail: the same for every bike of a model
static final int BIKE_BYTES = 48; // id, station, battery: different for every bike
// The flyweight: shared, never changed after creation (intrinsic state).
record BikeModel(String name, int maxKmh, boolean electric) {
String describe(String bikeId, String station, int battery) {
// per-bike (extrinsic) state is passed in, not stored here
String charge = electric ? ", battery " + battery + "%" : "";
return bikeId + ": " + name + ", max " + maxKmh + " km/h" + charge + ", at " + station;
}
}
// The flyweight factory: hands out one shared object per model name.
static class ModelRegistry {
final Map<String, BikeModel> models = new HashMap<>();
BikeModel get(String name, int maxKmh, boolean electric) {
return models.computeIfAbsent(name, n -> new BikeModel(n, maxKmh, electric));
}
}
// The per-bike object: small, and points at its shared model.
record Bike(String bikeId, BikeModel model, String station, int battery) {
String describe() { return model.describe(bikeId, station, battery); }
}
public static void main(String[] args) {
ModelRegistry registry = new ModelRegistry();
Object[][] specs = {{"Pedal classic", 20, false}, {"Pedal e-bike Mk2", 25, true}, {"Pedal cargo", 18, false}};
List<Bike> bikes = new ArrayList<>();
for (int i = 0; i < 40_000; i++) {
Object[] s = specs[i % 3];
boolean electric = (Boolean) s[2];
bikes.add(new Bike("B-" + (i + 1), registry.get((String) s[0], (Integer) s[1], electric), "S-12", electric ? 84 : 0));
}
System.out.println("bikes: " + bikes.size() + " | model objects: " + registry.models.size());
System.out.println("B-1 and B-4 share one model object: " + (bikes.get(0).model() == bikes.get(3).model() ? "True" : "False"));
System.out.println(bikes.get(1).describe());
long without = (long) bikes.size() * (BIKE_BYTES + MODEL_BYTES);
long withSharing = (long) bikes.size() * BIKE_BYTES + (long) registry.models.size() * MODEL_BYTES;
System.out.println(String.format(Locale.ROOT, "memory without sharing: %,d bytes", without));
System.out.println(String.format(Locale.ROOT, "memory with sharing: %,d bytes", withSharing));
}
}Output (the same in all three languages)
bikes: 40000 | model objects: 3 B-1 and B-4 share one model object: True B-2: Pedal e-bike Mk2, max 25 km/h, battery 84%, at S-12 memory without sharing: 169,920,000 bytes memory with sharing: 1,932,600 bytes
6. Try it
Shared or per bike?
Click a field to move it between the shared model and each bike. Then change the fleet size.
Highlighted fields live once per model; the others are stored on every bike. Sizes are illustrative.
7. When not to use it
What you get
- Large memory savings: when many objects share the same heavy data.
- One source of truth: update the model once and every bike sees it.
- Cheap identity checks: same model means the same object.
- Faster creation: no copying of the shared data.
What you pay
- State split in two: code must know which part lives where.
- Immutability required: shared state can't be changed through one object.
- Extra parameters: per-object state passed into flyweight methods.
- Little gain with few objects: for 50 bikes it's added complexity for kilobytes.
| If… | Use instead |
|---|---|
| Few objects, or little shared data | Plain objects |
| The data is big but needed only sometimes | Load it lazily: 09 Proxy |
| You want to reuse whole objects, not share parts | An object pool |
| The shared data changes per object | It isn't intrinsic; keep it on the object |
8. How it goes wrong
BikeModel would make every e-bike show the same charge. Only what's identical and fixed belongs there.model.max_kmh, one bug changes thousands of bikes. Make flyweights read-only.BikeModel(...) called directly creates duplicates, and the savings quietly disappear.9. Remember it and say it
How this hook works: what “One map on the wall” means
What the slogan means, phrase by phrase
- “One map”
- One flyweight per model.
- “on the wall”
- Shared and read-only.
- “carry what's theirs”
- Extrinsic state: id, station, battery.
Why this picture fits: A single map serves a thousand visitors because their differences (where they start) aren't on the map.
The story → the concept
| The library | The code |
|---|---|
| The wall map | BikeModel |
| The front desk that points you to it | ModelRegistry |
| Each visitor's start point | A bike's station and battery |
| Printing a map per visitor | 40,000 copies of the manual |
Watch it on an example
- The fleet screen loads B-2.
- The registry hands back the one shared e-bike Mk2 model.
- B-2 keeps only its id, station and battery, about 48 bytes.
The trap it saves you from
Copying identical, heavy data into thousands of small objects.
It still works when…
Yes: glyphs in text rendering, tiles in game maps, and interned strings all work this way.
In an interview or code review
- Give the numbers: "40,000 bikes, 3 models, 4.2 KB of model data: about 170 MB copied versus 1.9 MB shared."
- Name the split: intrinsic (model specs, manual) shared and immutable; extrinsic (id, station, battery) per bike.
- Mention the factory: one flyweight per key, so nobody creates duplicates by accident.
- Know the built-in ones: string interning, Java's
Integer.valueOfcache for small numbers.
10. Check yourself
What does a flyweight share?
Which field is extrinsic for a bike?
Why must flyweights be immutable?
What does the flyweight factory guarantee?
When is flyweight not worth it?
11. Go deeper
| Resource | Why |
|---|---|
| System design track | Memory budgets and caching at system scale. |
Python sys.intern | Sharing one copy of identical strings. |
Java Integer.valueOf(int) | A cached flyweight for small integers. |
| Wikipedia: Flyweight pattern | Intrinsic and extrinsic state, and history. |