InterviewKata
Design patterns · 12 of 26 · Fitting objects together

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

A library doesn't print a new copy of the city map for every visitor who asks the way. One map hangs on the wall; each visitor brings their own starting point and destination. What's the same for everyone is shared; what's different is carried by each person.

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

RoleIts jobIn Pedal
FlyweightHolds the shared, unchanging (intrinsic) state; takes per-object state as parameters.BikeModel
Flyweight factoryReturns the existing flyweight for a key, or creates it once.ModelRegistry.get(name, …)
ContextThe many small objects; hold their own (extrinsic) state and a reference to a flyweight.Bike: id, station, battery, model
ClientCreates 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, copied

With 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 changeWithout the patternWith it
Load 40,000 bikesAbout 170 MBAbout 1.9 MB
The e-bike manual is updated40,000 objects to update, or stale copiesOne BikeModel replaced
Ask "which bikes are the same model?"Compare field by fieldCompare 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.

Bikes in the fleet
Bike models

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 dataPlain objects
The data is big but needed only sometimesLoad it lazily: 09 Proxy
You want to reuse whole objects, not share partsAn object pool
The shared data changes per objectIt isn't intrinsic; keep it on the object

8. How it goes wrong

Putting changing state in the flyweightBattery level in BikeModel would make every e-bike show the same charge. Only what's identical and fixed belongs there.
Mutable flyweightsIf anyone can set model.max_kmh, one bug changes thousands of bikes. Make flyweights read-only.
Bypassing the factoryBikeModel(...) called directly creates duplicates, and the savings quietly disappear.
Optimising before measuringSplitting state for a list of 100 objects costs clarity and saves almost nothing. Measure first.

9. Remember it and say it

Memory hook: "One map on the wall"Share what's the same for everyone; let each one carry what's theirs.
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 libraryThe code
The wall mapBikeModel
The front desk that points you to itModelRegistry
Each visitor's start pointA bike's station and battery
Printing a map per visitor40,000 copies of the manual

Watch it on an example

  1. The fleet screen loads B-2.
  2. The registry hands back the one shared e-bike Mk2 model.
  3. 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

10. Check yourself

What does a flyweight share?

Per-object state stays out.

Which field is extrinsic for a bike?

It differs per bike and changes over time.

Why must flyweights be immutable?

Shared and mutable is a bug factory.

What does the flyweight factory guarantee?

Without it, duplicates creep back.

When is flyweight not worth it?

The saving is tiny and the split costs clarity.

11. Go deeper

ResourceWhy
System design trackMemory budgets and caching at system scale.
Python sys.internSharing one copy of identical strings.
Java Integer.valueOf(int)A cached flyweight for small integers.
Wikipedia: Flyweight patternIntrinsic and extrinsic state, and history.