01 / Notice the repetition
The first map did not need an object-sharing policy.
Creating a complete marker for every location is a reasonable start. Its position, name, style, and selection sit together. You can inspect one object and understand what will appear.
Now many locations repeat the same style descriptor. A real descriptor might include a vector path, label formatting rules, or other reusable appearance data. Ours deliberately uses a category, a glyph, and a fill color so you can follow every value.
Flyweight shares the part of an object’s state that can be reused across many occurrences, while keeping each occurrence’s context separate. A hospital style can be shared by hundreds of places. The selected hospital is still one particular location.
We will compare separate style objects with shared ones and preserve the visible result, counting the style objects each version retains.
02 / Draw the boundary
A marker has an appearance and a place to appear.
The pattern calls reusable, context-independent data intrinsic state. Data supplied by the occurrence is extrinsic state. Read those as “shared appearance” and “this place’s context” while following the map.
Keep the local facts.
ID, name, coordinates, and selection belong to the individual location.
Reuse an equal appearance.
Repeated hospital requests return the same immutable descriptor.
Combine them when drawing.
The renderer takes a glyph and fill from the style, then uses this place’s position and selection.
| Fact | Owner | Why? |
|---|---|---|
| Hospital glyph and fill | Shared style | Every hospital requests the same appearance. |
| Location ID and name | Each place | Equal appearance does not mean equal identity. |
| Coordinates | Each place | One style appears at many positions. |
| Selected or unselected | Each place | Selecting one location must not select its peers. |
These categories come from the example’s requirements. Color is shared here because category determines it. If each place needs its own tint, you could supply tint as local drawing context or request a distinct immutable style variant. “Color is always intrinsic” would be the wrong rule.
Sharing depends on identity. Two equal-looking objects still occupy two descriptor instances. The book’s repeated lookup returns the same instance, and the locations retain references to it.
03 / See the shape
Ask for a style instead of constructing it again.
The basic book creates a style on its first request and returns that object on later requests for the same category. There are only six categories, and each completely determines the descriptor.
The useful version generates the same fictional places under two layouts. Both keep selected on each place. drawRecord (Go’s Render) combines shared appearance
with local context; it does not need to know whether the style was reused.
A style book returns the same descriptor for repeated category requests. The implementations express the same identity boundary with language-appropriate tools, while the category fully determines the appearance in this fixed catalog.
export const categories = ['hospital', 'school', 'park', 'library', 'transit', 'shelter'] as const;
export type Category = (typeof categories)[number];
export type MarkerStyle = Readonly<{ category: Category; glyph: string; fill: string }>;
const appearances = {
hospital: ['H', '#b13f52'],
school: ['S', '#7754b3'],
park: ['P', '#297759'],
library: ['L', '#a4621f'],
transit: ['T', '#276fb1'],
shelter: ['E', '#956141']
} as const satisfies Record<Category, readonly [string, string]>;
export function makeStyle(category: Category): MarkerStyle {
const [glyph, fill] = appearances[category];
return Object.freeze({ category, glyph, fill }); // All fields are scalar values.
}
export class StyleBook {
#styles = new Map<Category, MarkerStyle>();
get(category: Category): MarkerStyle {
let style = this.#styles.get(category);
if (!style) {
style = makeStyle(category);
this.#styles.set(category, style);
}
return style;
}
} type Category string
const (
Hospital Category = "hospital"
School Category = "school"
Park Category = "park"
Library Category = "library"
Transit Category = "transit"
Shelter Category = "shelter"
)
// Unexported fields have no setters; callers treat a published style as immutable.
type MarkerStyle struct {
category Category
glyph, fill string
}
func MakeStyle(category Category) *MarkerStyle {
switch category {
case Hospital:
return &MarkerStyle{category, "H", "#b13f52"}
case School:
return &MarkerStyle{category, "S", "#7754b3"}
case Park:
return &MarkerStyle{category, "P", "#297759"}
case Library:
return &MarkerStyle{category, "L", "#a4621f"}
case Transit:
return &MarkerStyle{category, "T", "#276fb1"}
case Shelter:
return &MarkerStyle{category, "E", "#956141"}
default:
panic("unknown internal category")
}
}
type StyleBook struct{ styles map[Category]*MarkerStyle }
func (book *StyleBook) Get(category Category) *MarkerStyle {
if book.styles == nil {
book.styles = make(map[Category]*MarkerStyle)
}
if style, ok := book.styles[category]; ok {
return style
}
style := MakeStyle(category)
book.styles[category] = style
return style
} The generator accepts an integer count from zero through 5,000. An invalid count fails before constructing places. Selecting a valid, 1-based location ID clears the previous selection and selects that location. An invalid ID returns false and preserves the old selection. Locations and styles are ordinary internal model values, not a decoder for untrusted input.
Reading the TypeScriptMap identity and a frozen descriptor
Map keeps one descriptor per category. The book’s #styles field is private. A missing entry calls makeStyle; a present one returns
the original object. The set in styleCount counts object identities, so equal
fields do not merge separate descriptors.
Readonly describes the type contract. Object.freeze also prevents
changes to this object at runtime. Its fields are all scalar values, so there is no mutable
nested object left behind. If you later add arrays or nested records, shallow freezing is
no longer enough to make the whole descriptor immutable.
Reading the GoA pointer is the shared relationship
The book stores *MarkerStyle. Copying that pointer into another place
preserves the shared descriptor; copying the descriptor value itself would give the
place its own value. The map in StyleCount uses pointers as keys.
The descriptor’s fields are unexported and have no setters. Go does not make the
pointed-to struct immutable: code in the same package can still change it. Treat
publication as a read-only contract. SelectPlace updates each slice element by
index; changing a range-loop copy of a Place would not update the slice’s selection.
Reading the PythonFrozen dataclasses and identity sets
Python’s frozen, slotted MarkerStyle dataclass keeps the shared
descriptor value-like and prevents attribute changes at runtime. A mutable Place owns selection, while the StyleBook dictionary keeps one descriptor per category.
style_count uses id because dataclass equality is about
fields, not object identity. The duplicated layout therefore retains separate objects
even when their category, glyph, and fill are equal. ValueError rejects invalid counts; Python’s dynamic values also let the checker
demonstrate rejected fractional, non-finite, and boolean inputs.
04 / Predict, select, inspect
One click should still mean one hospital.
Start with 36 places. Inspect hospital 1 and predict how many locations will be selected. Switch between separate and shared styles: the descriptor count changes, but selection should not.
Then choose the deliberately broken version. It adds a mutable selection cell to each shared style group. This is the extra relationship that makes selecting one hospital select all of them. Expand the reference trace to see which places point to the same style.
Try 5,000 places after the small map makes sense. The model still has 5,000 locations. What changes is how many style descriptors they retain.
05 / Review the boundary
Sharing is safe when the shared facts agree.
A matching key promises that a descriptor can be reused without changing the requested result. That promise must remain true when appearance options grow, and it says nothing about whether locations should share mutable interaction state.
06 / Give the book an owner
Put the reuse where locations are assembled.
Imagine a public-service directory that receives location records from a data loader. The map owner resolves each record’s category to a style, stores the location’s own ID and coordinates, and gives the renderer that model. Selection events update the local place or a separate set of selected IDs.
In our source, one build creates one book. The book can disappear after construction because places keep the descriptors alive through their own references. A live map that adds records incrementally could retain its book for later requests. Give a second independent map its own scope unless there is a reason for broader sharing.
Now the directory adds dark and light appearances at the same time. Category alone no longer describes the reusable value. Key by category and theme, plus any other input that changes the descriptor. Keep coordinates and location IDs outside that key; adding them would prevent otherwise identical appearances from meeting.
When one hospital needs a special marker, request a new immutable variant or supply a local drawing override. Mutating the existing hospital descriptor would change every place that refers to it. For a deliberate global restyle, replace descriptors and update the relevant references as an explicit operation.
Lifetime and lookup are separate concernsStrong references, bounds, and concurrent access
This book can create at most six styles. An unbounded global book keyed by arbitrary colors, paths, and labels has a different retention problem: it can keep old styles alive long after a map closes. Define its scope and, if needed, a capacity or eviction policy.
Removing a book entry does not revoke references already held by places. A later lookup might create another equivalent descriptor while the old one remains live. That can preserve rendering but weaken the one-instance-per-key expectation across time. Define which guarantee actually matters.
The visible runtimes reclaim unreachable objects on their own schedules. A reference-counted implementation releases its value when its last strong owner is dropped. The Go book has no synchronization; sharing its mutable lookup table across goroutines would require a design for concurrent access.
What does the count prove?Identity, storage, and rendering cost
With 5,000 places using all six categories, the duplicated model retains 5,000 style descriptors. The shared model retains six. Both retain 5,000 place records and references from places to styles. The book also has its own lookup overhead during construction.
Those counts do not tell us the byte size of an object or whether strings, image data, or runtime constants were already shared. This tiny descriptor may be cheaper to keep simple. Larger repeated descriptors can make the tradeoff more useful; profile the application before presenting a saving as a measured result.
The lab still draws every marker, and its inspection records add their own allocations. Flyweight alone does not reduce draw calls, cluster map points, or virtualize a list.
The renderer’s flat draw records are value views. Serializing them as JSON would repeat fields and would not preserve object identity. Sharing inside a program and designing a compact wire format are different tasks.
Build UIs?Every icon sprite you color with currentColor is this split, and one day a table of prices will need its formatters shared.
Where it already is in your components
You probably already follow this rule if you use an SVG icon sprite: draw the icon with fill="currentColor", then color each icon by setting color on the button around it, not with a rule such as .hospital path. The reason
is this lesson’s split, run by the browser.
A <symbol> is the shared appearance: geometry defined once. Each <use href="#marker"> is an occurrence with its own position, size, and
inherited styles. The browser draws each use from a read-only copy of the
symbol and keeps every copy in step with the original, so changing the symbol’s path
redraws every icon. A click on one reports that <use> as its target: the
occurrence keeps its own identity, as one hospital keeps its own selection.
The copy is where the rule comes from. Its ancestors stop at the symbol, so a page rule
that reaches through the button, such as .hospital path, never matches it. The SVG 2 spec puts it this way: style rules in the main document “can only affect the shadow tree elements
by changing inherited values.” So each occurrence’s context arrives by inheritance. currentColor reads the button’s color, and a custom property
such as --marker-dot reaches a second shape. This is section 02’s decision
about color, made the other way: in a sprite, color is local context. Hard-code fill="#1f2937" on the path and you put color back into the shared definition;
in Chromium every marker stayed that gray whatever its button set. When the symbol sits in
the same component, Svelte’s compiler reports .hospital path as an “Unused CSS selector.”
<script lang="ts">
let { selected = 'hospital' }: { selected?: string } = $props();
const categories = ['hospital', 'school', 'park'];
</script>
<!-- One definition. Every button below draws this geometry. -->
<svg style="display: none">
<symbol id="marker" viewBox="0 0 24 24">
<!-- fill="#1f2937" here would beat every color the buttons set. -->
<path d="M12 2a7 7 0 0 0-7 7c0 5 7 13 7 13s7-8 7-13a7 7 0 0 0-7-7z" fill="currentColor" />
<circle cx="12" cy="9" r="2.5" style="fill: var(--marker-dot, white)" />
</symbol>
</svg>
{#each categories as category (category)}
<!-- Each use brings its own context: color from its button, the dot from selection. -->
<button class={category} aria-pressed={category === selected}>
<svg width="24" height="24" aria-hidden="true"><use href="#marker" /></svg>
{category}
</button>
{/each}
<style>
.hospital {
color: #b91c1c;
}
.school {
color: #1d4ed8;
}
.park {
color: #15803d;
}
[aria-pressed='true'] {
--marker-dot: gold;
}
/* .hospital path { fill: black } never reaches the copy: its ancestors stop at <symbol>. */
</style>
When you have to own it
Now your app shows a transactions table: thousands of rows in several currencies, each
amount formatted in its cell. Calling toLocaleString in every cell, or
building a new Intl.NumberFormat there, repeats setup the rows could share. MDN notes that each toLocaleString call “has to perform a search in a big
database of localization strings” and suggests reusing one formatter when the arguments
repeat. A formatter makes a good flyweight: you cannot change its options after creating
it, and the amount arrives with each format call.
Memoizing inside the row is not sharing. A row component that keeps its formatter in useMemo, or in a Svelte $derived, owns its own copy: with 500
rows, both frameworks constructed 500 formatters. The sharing belongs to whoever owns the
table, the way this section gave the map owner its style book: one book that returns a
formatter per key, used by every row.
Then choose the key the way this section added theme to category. A book keyed by locale
alone hands the first row’s dollar formatter to a euro row, which prints $950.00. Key by locale, currency, and every option you pass. The amount and
the row stay out of the key, supplied at each call.
07 / Recognize the idea
A definition can serve many occurrences.
Look for a stable description reused in several contexts: terrain definitions across a game map, glyph information at different positions in a document, or appearances across a visualization. The common question is which facts can be reused without changing the meaning of each occurrence.
Game objects make the split visible.
Robert Nystrom’s Flyweight chapter uses tree models and terrain types to explain shared data and per-instance context. Our marker descriptor plays a similar role: its appearance is reusable, while the location supplies where it is used.
Game Programming Patterns: Flyweight ↗GPU instancing adds another mechanism.
Three.js InstancedMesh supports many objects with the same geometry and material
but different transforms. It also addresses draw-call count through instanced rendering. The
shared-versus-local split is the useful connection.
08 / Make the call
Start with repeated data you can name.
Flyweight is a useful candidate when many live occurrences repeat substantial stable data, a small number of distinct descriptors can serve them, and the remaining context can stay with each occurrence. You should be able to name both sides of that split.
Keep a straightforward value when there are few occurrences, the descriptor is tiny, or nearly every appearance is unique. Sharing adds a lookup, references, and a lifetime to manage. If the shared part keeps needing per-place mutation, reconsider the boundary.
| Mechanism | Its central question | The distinction here |
|---|---|---|
| Flyweight | What stable data can many occurrences use at once? | Places simultaneously refer to immutable style descriptors. |
| Factory | Where do creation decisions live? | Our book also decides whether a descriptor already exists. |
| Multiton | Which callers should share one mutable instance per key? | Its get is the same lookup, create on miss, and retain as our book; there
the shared thing is settings callers change, here an immutable appearance. |
| Leased object pool | Which reusable resource can this caller acquire? | A style does not wait for another location to release exclusive access. |
| Result cache | Can a previous result answer this request? | A cached result can support a flyweight, but caching alone does not define the shared/local split. |
09 / Take the idea with you
The same appearance does not mean the same place.
Explain the idea without the name: “Each location points to an appearance object that other locations can also use. Its own identity, position, and selection stay local. Drawing combines the two.”
From memory: do 5,000 locations become six locations? Does selecting one hospital select all hospitals? Do six style objects prove a particular memory or rendering saving? All three answers are no in the correct example.
Find a repeated descriptor in code you know. List what belongs to the reusable value and what belongs to one occurrence. Then add a new appearance option. What must change in the key, and what must remain local?
Connections to follow nextRelated lessons
- Copying, identity, and equality explains why matching fields and sharing one object are different claims.
- Ownership, aliasing, and lifetimes helps reason about references that keep a shared descriptor alive.
- Factory gives creation decisions a home. A flyweight book adds a reuse policy to that boundary.