Low-level designDeep dive8 min

Flyweight

When thousands of objects repeat the same data, share it. Keep the repeated part in a few immutable objects handed out by a factory, and leave in each object only what is its own.

Watch it evolve1 / 8
The problem

A seating map of 12,000 seats

Each Seat holds its row and number, plus its tier, price and view photo.

80
Start
Stage 1
Final

1 · The situation

Twelve thousand seats, three kinds

An arena's ticketing site shows a seating map with 100 rows of 120 seats. Every Seat knows its row and number, its price tier (courtside, lower or upper), the tier's price, and a photo of the view from that tier, which the map shows when you hover over a seat.

The first version stores all of that in each seat. SeatingMap builds the seats row by row and picks the tier from the row number. In this sample a photo is represented by its size in kilobytes, 20 KB each, rather than real image data. Python writes view_photo_kb().

The first versionJava
import java.util.ArrayList;
import java.util.List;

class Seat {
    private final int row;
    private final int number;
    private final String tier;
    private final int priceCents;
    private final int viewPhotoKb;

    Seat(int row, int number, String tier, int priceCents, int viewPhotoKb) {
        this.row = row;
        this.number = number;
        this.tier = tier;
        this.priceCents = priceCents;
        this.viewPhotoKb = viewPhotoKb;
    }

    String label() {
        return tier + ", row " + row + " seat " + number + ": $" + priceCents / 100;
    }

    int viewPhotoKb() {
        return viewPhotoKb;
    }
}

class SeatingMap {
    private final int perRow;
    private final List<Seat> seats = new ArrayList<>();

    SeatingMap(int rows, int perRow) {
        this.perRow = perRow;
        for (int r = 1; r <= rows; r++) {
            for (int n = 1; n <= perRow; n++) {
                if (r <= 20) seats.add(new Seat(r, n, "Courtside", 18000, 20));
                else if (r <= 60) seats.add(new Seat(r, n, "Lower", 9500, 20));
                else seats.add(new Seat(r, n, "Upper", 4000, 20));
            }
        }
    }

    Seat seat(int row, int number) {
        return seats.get((row - 1) * perRow + number - 1);
    }

    int size() {
        return seats.size();
    }

    int photoKb() {
        int total = 0;
        for (Seat s : seats) total += s.viewPhotoKb();
        return total;
    }
}

public class Main {
    public static void main(String[] args) {
        SeatingMap map = new SeatingMap(100, 120);
        System.out.println("Seats: " + map.size());
        System.out.println(map.seat(30, 7).label());
        System.out.println(map.seat(85, 112).label());
        System.out.println("Tier data copies: " + map.size());
        System.out.println("View photo data: " + map.photoKb() + " KB");
    }
}
*Seat− row: int− number: int− tier: String− priceCents: int− viewPhotoKb: int~ label(): String~ viewPhotoKb(): intSeatingMap− perRow: int− seats: List<Seat>~ seat(int, int): Seat~ size(): int~ photoKb(): int

The class diagram of this version, drawn from its code: the map creates and owns every seat.

The map works, and the totals show what it holds:

Seats: 12000
Lower, row 30 seat 7: $95
Upper, row 85 seat 112: $40
Tier data copies: 12000
View photo data: 240000 KB

2 · The change

The map moves to phones

The site is building a mobile app that loads the whole map at once, and the phones run out of memory. The cause is in Seat: three of its five fields are the same for every seat of a tier.

The fields every seat copiesJava
class Seat {
    private final int row;
    private final int number;
    private final String tier;
    private final int priceCents;
    private final int viewPhotoKb;

    Seat(int row, int number, String tier, int priceCents, int viewPhotoKb) {
        this.row = row;
        this.number = number;
        this.tier = tier;
        this.priceCents = priceCents;
        this.viewPhotoKb = viewPhotoKb;
    }

    String label() {
        return tier + ", row " + row + " seat " + number + ": $" + priceCents / 100;
    }

    int viewPhotoKb() {
        return viewPhotoKb;
    }
}

There are 3 distinct photos and 12,000 copies of them, 240,000 KB of photo data for 60 KB of information. Tier names and prices are duplicated the same way, if less expensively. The data splits into two kinds:

Data

Same for every seat of a tier?

Count needed

Row and seat number

No: each seat's own

12,000

Sold or not

No: each seat's own

12,000

Tier name, price

Yes

3

View photo

Yes

3

3 · The refactor

Share what repeats, keep what's yours

Start from the first version. Move the data that repeats into its own class, make sure there's only one object per tier, and let each seat point to it:

SeatType holds a tier's name, price and photo, all fixed when it's made: there are no setters, so it's safe to share. Its label(row, number) takes the seat's own data as arguments.

Java
final class SeatType {
    private final String tier;
    private final int priceCents;
    private final int viewPhotoKb;

    SeatType(String tier, int priceCents, int viewPhotoKb) {
        this.tier = tier;
        this.priceCents = priceCents;
        this.viewPhotoKb = viewPhotoKb;
    }

    String label(int row, int number) {
        return tier + ", row " + row + " seat " + number + ": $" + priceCents / 100;
    }

    int viewPhotoKb() {
        return viewPhotoKb;
    }
}
*1*SeatType− tier: String− priceCents: int− viewPhotoKb: int~ label(int, int): String~ viewPhotoKb(): intSeatTypes− byTier: Map<String, SeatType>~ of(String, int): SeatType~ count(): int~ photoKb(): intSeat− row: int− number: int− type: SeatType~ label(): StringSeatingMap− perRow: int− types: SeatTypes− seats: List<Seat>~ seat(int, int): Seat~ size(): int~ typeCount(): int~ photoKb(): int

The map prints the same labels, from 3 tier objects instead of 12,000 copies:

Seats: 12000
Lower, row 30 seat 7: $95
Upper, row 85 seat 112: $40
Tier data copies: 3
View photo data: 60 KB
A copy in every seat
240,000
Shared seat types
60

View photo data held by the map, in kilobytes.

4 · The pattern

Flyweight: share the state that repeats

Flyweight cuts the memory of many similar objects by sharing their common part. The shared object, the flyweight, holds the intrinsic state: what doesn't depend on context and is the same wherever it's used. The extrinsic state, what differs from one use to the next, stays with the user, or is passed in when a flyweight method needs it. A factory makes sure each flyweight exists once.

*1*SeatType− tier: String− priceCents: int− viewPhotoKb: int~ label(int, int): String~ viewPhotoKb(): intSeatTypes− byTier: Map<String, SeatType>~ of(String, int): SeatType~ count(): int~ photoKb(): intSeat− row: int− number: int− type: SeatType~ label(): StringSeatingMap− perRow: int− types: SeatTypes− seats: List<Seat>~ seat(int, int): Seat~ size(): int~ typeCount(): int~ photoKb(): int

The roles in the arena's code: SeatType is the flyweight, SeatTypes its factory, and each Seat a context holding the extrinsic state.

Role

In the arena

Its job

Flyweight

SeatType

Holds the shared, intrinsic state; immutable

Flyweight factory

SeatTypes

Creates each flyweight once and hands out the same object after

Context

Seat

Holds the extrinsic state and a reference to its flyweight

Client

SeatingMap

Asks the factory for flyweights and builds the contexts

The first request for a tier creates its flyweight; every later one gets the same object back.

Sharing doesn't make a price change free. The flyweight is immutable, so a new price means a new SeatType, and pointing the factory at it only affects seats made afterwards: every existing seat of that tier still has to be moved to the new object, or the map rebuilt. Editing the shared one in place is no shortcut: it gives up the immutability that makes sharing safe, and a basket that already quoted the old price would see it change underneath. When prices move often, keep them out of the flyweight, in a price list the seats look up by tier.

5 · What it costs

Memory saved, simplicity spent

  • The split adds a class and a factory. One plain class became three, for a saving that only matters at scale.
  • Extrinsic state travels. Anything a flyweight needs about its context must be passed in, as label(row, number) is. With many such values the methods get long argument lists.
  • Shared means immutable. A setter on SeatType would change every seat of the tier at once. Shared objects must not change after they're made.
  • The factory is shared too. If several threads build maps at once, the factory's map needs locking or a concurrent map, or two threads can each create a tier.

Situation

Flyweight?

Thousands of objects repeat large, identical data

Yes

A few hundred objects, or little repeated data

No: the saving doesn't pay for the split

The repeated data changes per object over time

No: it isn't intrinsic

The language already shares equal immutable values for you

Often unnecessary; check what it shares first

6 · Getting it right

Immutable, keyed by everything that defines it

  • Make the flyweight immutable. Final fields in Java, const members in C++, private fields with no setters in JavaScript, a frozen dataclass in Python.
  • Key the factory by everything intrinsic. SeatTypes.of() is keyed by tier alone, so asking for "Lower" at a different price returns the first price silently. Key by the full set of intrinsic values, or reject a mismatch.
  • Measure first. Count the objects and the bytes before splitting; the pattern is a memory optimization, and it adds code.
  • Keep extrinsic state out of the flyweight. If you find yourself adding a seat number to SeatType, it's no longer shareable.

Language

Sharing and immutability

Java

Final fields; a HashMap in the factory, or a ConcurrentHashMap when threads share it

C++

const members; the factory owns flyweights in a std::map, whose elements keep their addresses, and contexts hold pointers to them

JavaScript

Private fields with no setters; a Map keyed by the intrinsic values

Python

A frozen dataclass; a dictionary in the factory

7 · Look-alikes

Sharing, reusing and copying

Flyweight is one of several patterns about how many instances exist. What differs is why, and whether the instances can change:

Pattern

How many instances

Shared or reused

Mutable?

Flyweight

One per distinct intrinsic value

Shared by many at once

No

Singleton

One in total

Shared by everyone

Often yes

Object Pool

A fixed set

Lent to one user at a time, then returned

Yes, reset between users

Prototype

As many as needed

Copied from a template, not shared

Yes

A flyweight is usually a value object, as described in Value objects, equality and immutability: equal by its contents, never changed. Flyweight adds the factory that ensures equal values are also the same object.

8 · In practice

Flyweights in the standard library

Java's Integer.valueOf(int) always returns cached objects for values from -128 to 127, so code that boxes small numbers shares a few hundred Integer objects instead of creating new ones. Text editors and renderers share glyph data across every occurrence of a character, with the position passed in as extrinsic state.

The practice checks have no Flyweight detector, and both versions here read as clean. The pattern is a structural memory decision; the checks see three ordinary classes and a map.

Check yourself

4 questions

1. The arena adds a "Box" tier for 200 seats. What changes?
2. A developer adds setPrice() to SeatType to change one seat's price. What happens?
3. Which data belongs in the flyweight?
4. SeatTypes.of("Lower", 9000) is called after of("Lower", 9500). What does it return?

Takeaways

Remember this

  • Flyweight shares the state that many objects repeat, in a few immutable objects.
  • Intrinsic state is shared and fixed; extrinsic state stays with each object or is passed in.
  • A factory makes sure each distinct flyweight is created once and reused.
  • Shared objects must be immutable, or one change reaches every user.
  • It is a memory optimization: measure before you split.