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:
// 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.
| Fact | Owner | Reason |
|---|---|---|
| Hospital glyph and size | Shared style | Every small hospital has the same appearance. |
| ID and coordinates | Each place | Equal appearance does not make places identical. |
| Selection | Each place | Selecting 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.
// 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}`); | Role | What it owns | In the program |
|---|---|---|
| Style | What every marker of one kind looks like | A frozen MarkerStyle, shared |
| Book | One style per key, made on the first request | StyleBook.get(category, size) |
| Place | ID, position, selection, and which style it uses | A 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
// Line 14 becomes: the book keys each style by its category alone.
const key = category; 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.
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.
// 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.
| The change | A complete marker per place | Shared 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 tint | Change 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 redesigned | Every 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. |
| Choice | What it owns |
|---|---|
| Flyweight | Immutable data used simultaneously by many occurrences. |
| Multiton | One mutable instance per owner and key. |
| Object pool | Exclusive temporary use of a reusable resource. |
| Result cache | A 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?
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