← Design patterns
Composition Shared data, local context

Flyweight

Share the appearance. Keep the place.

A directory map begins with a complete marker per location: ID, coordinates, style, and selection. That is easy to inspect. When the map grows to thousands of places, each hospital still constructs the same appearance descriptor. We can keep one immutable descriptor for each repeated appearance and let every place refer to it. Position and selection stay with the individual place.

TypeScriptGoRust The same style book in each language.

Definition

The Flyweight pattern uses sharing to support large numbers of fine-grained objects efficiently: the state they have in common is stored once, in shared objects, and each use supplies the state that is its own.

After Gamma et al., Design Patterns (1994), Flyweight

01Start with the complete marker

Every place has its own identity.

With a handful of locations, putting all marker data together is a good design. The object is complete, and a click can update its own selection flag. Here each place builds its own marker as the map loads, style included:

places.ts
// Each place builds a complete marker, its own copy of the style included.
const places = rows.map((row) => ({
  id: row.id,
  x: row.x,
  y: row.y,
  style: { glyph: glyphs[row.category], size: row.size },
  selected: false,
}));

(A fragment: rows is the map’s loaded data, and the types are the ones in the program below.) At 5,000 locations, many descriptors repeat. A descriptor can contain a category glyph, a size, a fill, a vector path, or other stable appearance data. In this example a glyph and a size stand for it, so the shared part is easy to inspect.

Flyweight shares stable state that many live occurrences can use at once. We call that reusable part intrinsic state. The location’s ID, coordinates, and selection are extrinsic state: each place supplies them for its own occurrence.

One appearance, many locations
FactOwnerReason
Hospital glyph and sizeShared styleEvery small hospital has the same appearance.
ID and coordinatesEach placeEqual appearance does not make places identical.
SelectionEach placeSelecting one place must leave its peers alone.

That split follows the requirement, not a universal rule about which fields are intrinsic: it holds as long as every hospital of one size really looks the same.

02Share the descriptor

A small book reuses a style on a key hit.

A small book hands out the shared part. The first request for a kind of marker creates its style; later requests return the same object. One map owns the book, and the places keep references to the styles they use. This is the whole program the lab runs: the book, four places, and one selection.

markers.ts
// Shared: how every marker of one kind looks. Nothing changes it.
type MarkerStyle = Readonly<{ glyph: string; size: string }>;
// Local: what belongs to one place on the map.
type Place = {
  id: number; x: number; y: number; style: MarkerStyle; selected: boolean;
};

const glyphs: Record<string, string> = { hospital: 'H', school: 'S', park: 'P' };

// The flyweight factory: one style per kind of marker, made on first request.
class StyleBook {
  private styles = new Map<string, MarkerStyle>();
  get(category: string, size: string): MarkerStyle {
    const key = `${category}/${size}`;
    let style = this.styles.get(key);
    if (!style) {
      style = Object.freeze({ glyph: glyphs[category], size });
      this.styles.set(key, style);
    }
    return style;
  }
  get count(): number {
    return this.styles.size;
  }
}

// One map, one book. Place 3 is the trauma centre, drawn large.
const place = (id: number, x: number, y: number, style: MarkerStyle): Place =>
  ({ id, x, y, style, selected: false });
const book = new StyleBook();
const places = [
  place(1, 12, 40, book.get('hospital', 'small')),
  place(2, 30, 18, book.get('school', 'small')),
  place(3, 47, 62, book.get('hospital', 'large')),
  place(4, 65, 25, book.get('hospital', 'small')),
];

// Selecting a place changes that place, never the style it shares.
function selectPlace(places: Place[], id: number) {
  for (const place of places) place.selected = place.id === id;
}
selectPlace(places, 4);

const shared = places[0].style === places[3].style;
const styles = book.count;
for (const p of places) {
  const mark = p.selected ? ' (selected)' : '';
  const { glyph, size } = p.style;
  console.log(`#${p.id} ${glyph} ${size} at ${p.x},${p.y}${mark}`);
}
console.log(`${places.length} places, ${styles} styles`);
console.log(`#1 and #4 share one style: ${shared}`);
Who owns what
RoleWhat it ownsIn the program
StyleWhat every marker of one kind looks likeA frozen MarkerStyle, shared
BookOne style per key, made on the first requestStyleBook.get(category, size)
PlaceID, position, selection, and which style it usesA Place per location

The caller still owns the dataset and the selection; whatever draws the map combines the style’s glyph and size with one place’s coordinates and selection.

03Predict what the map draws

Change one line of the book. What does the map draw?

Experiment

Four places, one style book

markers.ts
// Line 14 becomes: the book keys each style by its category alone.
    const key = category;
Suppose the book keyed each style by its category alone: the line that builds key becomes the one below. Place 3, the trauma centre, asks for a large hospital marker after place 1 asked for a small one. What does place 3 draw?
What a production style book addsValidation, a full key, and a count by identity

The same book, checked. An unknown category or size is an error, and a failed request keeps nothing. The key holds every fact that changes the appearance: category and size, validated first, so no value can contain the separator. Selection reports whether it found the place and changes nothing when it did not, and styleCount counts the distinct styles places hold by identity, not by equal fields.

markers.ts
const glyphs: Record<string, string> = {
	hospital: 'H',
	school: 'S',
	park: 'P',
	library: 'L',
	transit: 'T',
	shelter: 'E'
};
const sizes = ['small', 'large'];

// Shared: how every marker of one kind looks. Frozen, so no holder can change it.
export type MarkerStyle = Readonly<{ glyph: string; size: string }>;

// Local: what belongs to one place on the map.
export type Place = {
	readonly id: number;
	readonly x: number;
	readonly y: number;
	readonly style: MarkerStyle;
	selected: boolean;
};

// One map owns one book; a style lives as long as some place still holds it.
export class StyleBook {
	#styles = new Map<string, MarkerStyle>();

	get(category: string, size: string): MarkerStyle {
		// hasOwn: "toString" is not a category, though every object has one.
		if (!Object.hasOwn(glyphs, category)) throw new Error(`unknown category: ${category}`);
		if (!sizes.includes(size)) throw new Error(`unknown size: ${size}`);
		// The key holds every fact that changes the appearance.
		const key = `${category}/${size}`;
		let style = this.#styles.get(key);
		if (!style) {
			style = Object.freeze({ glyph: glyphs[category]!, size });
			this.#styles.set(key, style);
		}
		return style;
	}

	get count(): number {
		return this.#styles.size;
	}
}

// Selects the place with this id and clears the rest; an unknown id changes nothing.
export function selectPlace(places: Place[], id: number): boolean {
	if (!places.some((place) => place.id === id)) return false;
	for (const place of places) place.selected = place.id === id;
	return true;
}

// How many distinct style objects the places hold: identity, not equal fields.
export function styleCount(places: readonly Place[]): number {
	return new Set(places.map((place) => place.style)).size;
}

The book has at most twelve entries here: six categories in two sizes. Each map owns one, and closing or replacing the map ends it; places keep their styles alive as long as they hold them.

Reading the TypeScriptMap identity, ===, and a frozen descriptor

Map keeps one descriptor per key, and get returns that same object, so === compares identity: two places share a style when their style fields are one object, not when their fields are equal. A Set of styles counts identities the same way.

Readonly states the type contract, while Object.freeze prevents runtime changes to this flat record. A later nested field would need its own immutability decision: freezing is shallow.

04Meet it in your language

You already share objects like this.

A statement export formats thousands of amounts. MDN’s page on toLocaleString advises creating one Intl.NumberFormat and reusing it when you format many numbers with the same options, so a module keeps a book of formatters. The bug is the key: a book keyed by locale alone hands a euro row the formatter the first dollar row made.

formatters.ts
// A statement export formats thousands of amounts with a few option sets.
// Before: one formatter per locale, whatever the currency.
const byLocale = new Map<string, Intl.NumberFormat>();

export function formatBefore(amount: number, locale: string, currency: string) {
  let format = byLocale.get(locale);
  if (!format) {
    format = new Intl.NumberFormat(locale, { style: 'currency', currency });
    byLocale.set(locale, format);
  }
  return format.format(amount);
}

// After: the key holds every option that changes the output.
const formats = new Map<string, Intl.NumberFormat>();

export function formatAmount(amount: number, locale: string, currency: string) {
  const key = `${locale}|${currency}`;
  let format = formats.get(key);
  if (!format) {
    format = new Intl.NumberFormat(locale, { style: 'currency', currency });
    formats.set(key, format);
  }
  return format.format(amount);
}

formatAmount keys by locale and currency, every option that changes the output; if the export later sets minimumFractionDigits, that goes in the key too. The book is a module-level map, so it lives as long as the process: fine for a few locales and currencies, but bound it if the keys come from user input.

05Keep the simple value when it fits

Share only when the saved object is worth a boundary.

A plain per-place value is clear and independent. Keep it when there are few places, the descriptor is tiny, or most appearances are unique. A flyweight book adds a lookup, references, key rules, and a lifetime to manage; the count of styles is not a memory or speed measurement.

For many occurrences with a small set of stable, substantial descriptors, shared identity can be useful. The book belongs to the map owner. Closing or replacing that map ends the book’s scope; place references keep their descriptors alive as long as needed.

What changes when another requirement arrives?
The changeA complete marker per placeShared styles from a book
A new appearance option arrives (a dark theme)Each place builds the variant it needs.Extend the key and the style together; every option that changes the look is part of the key.
One place needs a special tintChange that place’s own style.Ask the book for a new immutable variant, or pass the tint as drawing context. Never change the shared style.
The hospital glyph is redesignedEvery new place gets the new glyph.A new book makes new styles. Places built from the old book keep the old style until they are rebuilt.
Three nearby mechanisms, three different jobs
ChoiceWhat it owns
FlyweightImmutable data used simultaneously by many occurrences.
MultitonOne mutable instance per owner and key.
Object poolExclusive temporary use of a reusable resource.
Result cacheA previous answer that can satisfy a request; it does not alone define the shared/local boundary.

Multiton is the closest neighbour in code: both keep one object per key in an owner. A multiton’s object is mutable and its updates are the point; a flyweight’s is immutable and sharing it is invisible. Object pool reuses objects one user at a time, and a factory is how a book makes each style on a miss.

Production detailsBounds, lookup changes, and what identity counts

This sample has a bounded set of keys. A process-wide book keyed by arbitrary paths, labels, or colors can retain obsolete variants; scope it to an owner and define a bound if keys can grow. Removing a book entry does not revoke references already held by places.

With 5,000 places and a dozen kinds of marker, the duplicated model has 5,000 style identities and the shared model has twelve. Both still have 5,000 place records and their references. These counts say nothing about bytes, decoded assets, draw calls, clustering, or speed; profile a real workload before claiming a saving.

When a new appearance option arrives, extend the key. A custom marker can use a new immutable variant or local drawing context; mutating a shared descriptor changes every place that refers to it.

06Make the call

What is shared, what is local, and who owns the book?

A settings screen draws twelve icons, each with its own glyph and colour. None repeats. How should their styles be held?
A map accepts unlimited custom marker styles from its users. Where should the retained styles be managed?
A spreadsheet shares one format object (font, fill, number format) among all cells that look alike, from a book keyed by those fields; each cell keeps its own value. A user makes one cell bold. What should change?

Before you open the answer below, say it in your own words: which facts go in the shared object, what must its key hold, and who decides how long the book lives?

One way to say itIntrinsic, extrinsic, the key and the owner

Share the facts that are the same for every occurrence and never change while they are used, the intrinsic state, and keep everything that belongs to one occurrence, its identity, position and selection, with the occurrence. The book makes each shared object on the first request and returns that same object after, so its key must hold every fact that changes the appearance; a missing fact makes the first request decide for all the later ones. The shared object never changes: a different look is a different object from the book. And the book has an owner, the map, whose lifetime ends it. Keep a plain value per occurrence when little repeats; the pattern earns its lookup and its lifetime only when many live occurrences share a few substantial descriptors.

Practice in the lab

One step from the lesson’s map: a seat map is a map whose places are seats. The box office draws the arena’s seats from a few zones, each with a ticket price and a colour, and customers hold seats at the counter. Each level starts from the previous level’s finished code, so you can begin at any of them.

  • FLYWEIGHT-101 (easy) Eighteen thousand copies of three zones
  • FLYWEIGHT-102 (medium) Accessible spaces draw like every other seat Pro
  • FLYWEIGHT-103 (hard) Review: one style book for the whole box office Pro