---
title: Flyweight
source: 'https://academia.sh/en/courses/design-patterns/flyweight'
course: 'Design Patterns'
language: en
updated: '2026-08-23T07:01:15+00:00'
license: 'CC BY-SA 4.0'
---

# Flyweight

Saving memory through sharing: comparing, by object count and a flagged heap measurement, the arrangement where every shipment carries its own tariff copy during batch fee calculation against the arrangement gathering the unchanging tariff information into a shared object; the cost of sharing in the immutability it requires and its sharing width.

In the previous four patterns, the measure was files, types, and dependency count; all of
them looked at the size of the source text. This lesson's measure lives at run time:
memory.

A batch fee job processes two hundred thousand shipment rows. Every shipment needs a piece
of tariff information: the weight-tier table, the zone coefficients, the currency. This
information's content **does not vary** by shipment — it is identical across every shipment
carrying the same tariff code. What varies by shipment is only weight and address. The
**flyweight** pattern separates these two: the unchanging part is shared as **intrinsic
state** in a single object, the varying part stays on the record itself as **extrinsic
state**. The measures are the object count built and heap usage.

## Problem: Two Hundred Thousand Copies of the Same Table

The raw rows and the tariff tables are common to both arrangements. Since the rows are
derived from the index, they are the same on every run; there is no randomness.

```js
// data.mjs — raw shipment rows and tariff tables; values are derived from the index
export const TIER_TABLE = {
  standard: [[1, 4990], [5, 8490], [15, 14990], [30, 24990]],
  economy: [[1, 3990], [5, 6990], [15, 11990], [30, 19990]],
  express: [[1, 6490], [5, 10990], [15, 18990], [30, 31990]],
};
export const COEFFICIENT_TABLE = { 34: 100, "06": 118, 35: 120, 65: 145 };
export const TARIFF_CODE = ["standard", "economy", "express"];
const POSTAL = ["34100", "06500", "35400", "65100"];

export function* rawRows(n) {
  for (let i = 0; i < n; i += 1) {
    yield {
      code: `G-${i}`,
      weight: 0.5 + (i % 59) / 2,
      postalCode: POSTAL[i % POSTAL.length],
      tariffCode: TARIFF_CODE[i % TARIFF_CODE.length],
    };
  }
}
```

In the first arrangement, every record builds its own tariff object. This is the most
natural way not to share the tables: a record is self-sufficient and shares nothing.

```js
// copied/main.mjs — every shipment carries its own tariff copy
import { TIER_TABLE, COEFFICIENT_TABLE } from "../data.mjs";

let created = 0;
export const createdTariffs = () => created;

export function build(rows) {
  const list = [];
  for (const s of rows) {
    created += 1;
    list.push({
      code: s.code,
      weight: s.weight,
      postalCode: s.postalCode,
      tariff: {
        code: s.tariffCode,
        tiers: TIER_TABLE[s.tariffCode].map(([u, f]) => [u, f]),
        coefficients: { ...COEFFICIENT_TABLE },
        currency: "cents",
      },
    });
  }
  return list;
}

export function fee(g) {
  const t = g.tariff.tiers.find(([cap]) => g.weight <= cap) ?? [0, 24990];
  return Math.round((t[1] * (g.tariff.coefficients[g.postalCode.slice(0, 2)] ?? 165)) / 100);
}
```

## Solution: A Pool Sharing Intrinsic State

The flyweight factory returns a single object for the same intrinsic state. The object
must be **immutable**; the reason is measured below.

```js
// shared/tariff.mjs — flyweight factory: one object per tariff code
import { TIER_TABLE, COEFFICIENT_TABLE } from "../data.mjs";

const pool = new Map();
let created = 0;

export const createdTariffs = () => created;
export const poolSize = () => pool.size;

export function getTariff(code) {
  let t = pool.get(code);
  if (t === undefined) {
    created += 1;
    t = Object.freeze({
      code,
      tiers: Object.freeze(TIER_TABLE[code].map(([u, f]) => Object.freeze([u, f]))),
      coefficients: Object.freeze({ ...COEFFICIENT_TABLE }),
      currency: "cents",
    });
    pool.set(code, t);
  }
  return t;
}
```

```js
// shared/main.mjs — a shipment holds a reference to the tariff; intrinsic state is shared, extrinsic state stays on the row
import { getTariff, createdTariffs, poolSize } from "./tariff.mjs";

export { createdTariffs, poolSize };

export function build(rows) {
  const list = [];
  for (const s of rows) {
    list.push({ code: s.code, weight: s.weight, postalCode: s.postalCode, tariff: getTariff(s.tariffCode) });
  }
  return list;
}

export function fee(g) {
  const t = g.tariff.tiers.find(([cap]) => g.weight <= cap) ?? [0, 24990];
  return Math.round((t[1] * (g.tariff.coefficients[g.postalCode.slice(0, 2)] ?? 165)) / 100);
}
```

The fee calculation's body is identical in both arrangements; what changes is what
`g.tariff` belongs to.

```js
// run.mjs — do the two arrangements give the same total fee, and how many tariff objects get built
import { rawRows } from "./data.mjs";
import * as copied from "./copied/main.mjs";
import * as shared from "./shared/main.mjs";

const N = 20000;
const total = (arrangement) => arrangement.build(rawRows(N)).reduce((t, g) => t + arrangement.fee(g), 0);

const a = total(copied), b = total(shared);
console.log(`copied: total=${a}  tariff objects=${copied.createdTariffs()}`);
console.log(`shared: total=${b}  tariff objects=${shared.createdTariffs()}`);
console.log(`total difference = ${a - b}  shipments = ${N}`);
```

```
copied: total=463093345  tariff objects=20000
shared: total=463093345  tariff objects=3
total difference = 0  shipments = 20000
```

For twenty thousand shipments, the copied arrangement built 20,000 tariff objects, the
shared arrangement 3. The total fee matches to the cent. Object count is an exact number
independent of environment: as many objects get built as there are varieties of intrinsic
state.

## Heap Measurement

The second measure is heap usage, and it is **environment-dependent**: the megabyte values
below were taken on this machine with `node` v24.18.0; the numbers change when the version,
operating system, or garbage collector settings change. What does not change is the order
of magnitude. The measurement requires calling the garbage collector by hand, so the script
runs with the `--expose-gc` flag.

```js
// memory.mjs — builds one arrangement and measures heap usage; run with node --expose-gc
import { rawRows } from "./data.mjs";

if (globalThis.gc === undefined) {
  console.error("this script is run as 'node --expose-gc memory.mjs <arrangement> <count>'");
  process.exit(1);
}

const arrangement = process.argv[2];
const count = Number(process.argv[3]);
const { build, fee, createdTariffs } = await import(`./${arrangement}/main.mjs`);

globalThis.gc();
const before = process.memoryUsage().heapUsed;
const list = build(rawRows(count));
globalThis.gc();
const after = process.memoryUsage().heapUsed;
const total = list.reduce((t, g) => t + fee(g), 0);
console.log(`${arrangement.padEnd(11)} shipments=${list.length}  tariff objects=${createdTariffs()}  ` +
  `heap=${Math.round((after - before) / 1048576)} MB  total=${total}`);
```

```sh
node --expose-gc memory.mjs copied 200000
node --expose-gc memory.mjs shared 200000
```

```
copied      shipments=200000  tariff objects=200000  heap=208 MB  total=4631039370
shared      shipments=200000  tariff objects=3  heap=20 MB  total=4631039370
```

208 megabytes against 20 megabytes for two hundred thousand shipments — a ratio a little
above tenfold. The remaining 20 megabytes is the extrinsic state itself — shipment code,
weight, postal code — and cannot be shrunk by sharing, because it differs in every record.
The measurement was taken in two separate processes; building both arrangements in the same
process would risk the first arrangement's leftover objects bleeding into the second
measurement.

The total fee came out the same in both runs; sharing did not change the result.

## Cost: Immutability and Sharing Width

The shared object's most visible cost is that it no longer belongs to anyone.

```js
// cost.mjs — the cost of sharing: mutability, sharing width, object identity
import { rawRows } from "./data.mjs";
import * as copied from "./copied/main.mjs";
import * as shared from "./shared/main.mjs";

const N = 20000;
const k = copied.build(rawRows(N));
const p = shared.build(rawRows(N));

const previousCopied = k.map(copied.fee);
k[0].tariff.coefficients["34"] = 300;
console.log(`copied: one record's table changed -> changed results = ` +
  `${k.map(copied.fee).filter((v, i) => v !== previousCopied[i]).length} / ${N}`);

try {
  p[0].tariff.coefficients["34"] = 300;
  console.log("shared: same change accepted");
} catch (e) {
  console.log(`shared: same change rejected (${e.constructor.name})`);
}

console.log(`shared: shipments sharing the same object = ${p.filter((g) => g.tariff === p[0].tariff).length} / ${N}`);
console.log(`copied: shipments sharing the same object = ${k.filter((g) => g.tariff === k[0].tariff).length} / ${N}`);
const variety = shared.poolSize();
console.log(`intrinsic state variety = ${variety}  sharing ratio = ${Math.round(N / variety)} shipments/object`);
```

```
copied: one record's table changed -> changed results = 1 / 20000
shared: same change rejected (TypeError)
shared: shipments sharing the same object = 6667 / 20000
copied: shipments sharing the same object = 1 / 20000
intrinsic state variety = 3  sharing ratio = 6667 shipments/object
```

In the copied arrangement, changing one record's table broke only that record: 1/20000. In
the shared arrangement, the same change was rejected by `Object.freeze` and raised a
`TypeError`. Without the freeze, the change would have been accepted, and the 6667
shipments sharing that object would have silently drifted. The sharing ratio measures both
the gain and the risk: every object serves 6667 records, so a bug in that object propagates
to 6667 records.

The second cost is that object identity changes meaning. In the copied arrangement, a
`g1.tariff === g2.tariff` comparison holds only for the same record; in the shared
arrangement, it holds for every shipment carrying the same tariff code. Code that infers,
from identity equality, something like "if these two shipments use the same tariff object,
they belong to the same customer" gets it wrong once sharing is introduced.

The third cost is the pool itself: one file, one map structure, and one lookup while
building every record. The pool also never empties; if intrinsic-state variety is
unbounded, the pool grows unbounded.

A condition for not applying the pattern follows directly. The gain grows with the ratio of
record count to intrinsic-state variety: 200,000/3 in this run. If the ratio approaches 1 —
if every record's intrinsic state is its own — there is nothing to share, the pool holds as
many objects as there are records, and a mapping overhead gets added on top. If the record
count is small (hundreds), the 200-megabyte difference shrinks to a hundred kilobytes, and
the immutability constraint goes unpaid for.

## Summary

- The problem the flyweight solves is unchanging information filling memory by getting
  copied once per record; the solution is gathering intrinsic state into a shared object
  and leaving extrinsic state on the record.
- For twenty thousand shipments, the tariff objects built dropped from 20,000 to 3, and the
  total fee stayed the same to the cent; object count is a measure independent of
  environment.
- For two hundred thousand shipments, heap usage dropped from 208 megabytes to 20; these
  values were measured on this machine with `node` v24.18.0 and change with the
  environment, while the ratio holds as an order of magnitude.
- The shared object must be immutable: in the copied arrangement, one table change broke 1
  record; in the shared arrangement, the same change was rejected thanks to the freeze —
  had it been accepted, it would have broken 6667 records sharing that object.
- The gain grows with the ratio of record count to intrinsic-state variety; if the ratio is
  close to 1 or the record count is small, the pattern only adds pool overhead and an
  immutability constraint.

## Next Step

In the flyweight, many records reached one object through the same reference; the object
was shared, but access was unrestricted — anyone could read the `tariff` field and use it
directly. The next problem takes access itself as its subject. If the tariff table comes
from a remote source, it must not load until the first access, a second request for the
same table must not fetch it again, and it must be possible to count which record requested
which table. All three get solved, without touching the real object's body, by an object
offering the same interface stepping in between. The next lesson counts the calls reaching
the source in both arrangements, writes the proxy pattern, and measures its cost in a level
of indirection and stale-data risk.
