# Flyweight

> Share one immutable object for state that many objects have in common, and pass the per-object rest in, to cut memory when instances number in the millions.

- kind: pattern
- category: structural
- aliases: Interning, Hash Consing
- canonical: https://aipatterns.dev/design-patterns/flyweight
- inspired-by: https://refactoring.guru/design-patterns/flyweight

## Intent

Fit far more objects in memory by splitting each object's state in two. The *intrinsic* part, identical across many objects, moves into a shared, immutable flyweight. The *extrinsic* part, unique to each object, stays with the object or is passed into the flyweight's methods. A factory hands out one flyweight per distinct intrinsic value.

## Forces

- **Millions of fine-grained objects** vs **finite memory**: a spreadsheet with 600,000 cells cannot afford 600,000 copies of the same font, size and number format.
- **Sharing** vs **mutability**: a shared object that one owner changes silently changes for every other owner.
- **Object-oriented clarity** vs **memory layout**: each cell "having a style" reads well; each cell *owning* a style wastes space.
- **Saving memory** vs **spending CPU**: lookups in the flyweight pool and passing extrinsic state around cost time.

## Therefore

**Move the repeated, immutable state into a shared object, obtain instances only through a factory that returns an existing one when an equal value is requested, and keep or pass the varying state outside it.**

The flyweight must be immutable. Without that, sharing is a bug. The factory is a map from the intrinsic value to the one shared instance. In many languages this is a few lines, and sometimes the runtime already does it for you (string interning, enums, small integer caches).

## Structure

```mermaid
classDiagram
  class CellStyle {
    -String font
    -int sizePt
    -boolean bold
    -String numberFormat
    +format(value) String
  }
  class StyleRegistry {
    -Map pool
    +get(font, sizePt, bold, numberFormat) CellStyle
    +size() int
  }
  class Cell {
    -int row
    -int col
    -double value
    -CellStyle style
  }
  class Sheet {
    -Cell[] cells
    +set(row, col, value, style) void
  }
  StyleRegistry o-- CellStyle : pool
  Cell --> CellStyle : shares
  Sheet *-- Cell
  Sheet ..> StyleRegistry
```

### Participants

- **Flyweight** (`CellStyle`): immutable intrinsic state. Methods take extrinsic state as arguments (`format(value)`).
- **Flyweight factory** (`StyleRegistry`): returns the existing instance for an equal key, or creates and stores a new one.
- **Context** (`Cell`): holds the extrinsic state (position, value) and a reference to a shared flyweight.
- **Client** (`Sheet`): creates contexts and gets flyweights only through the factory, never with `new`.

## Use it when

- A profiler or heap dump shows millions of objects with duplicated field values dominating memory.
- Most of each object's state can be made immutable and is shared by many objects: styles, glyph metrics, tile types, map-marker icons, parsed schema fragments.
- The remaining per-object state is small or can be computed from context (position, index).
- Object identity does not matter for the shared part; equal values may be the same instance.

## Do not use it when

- You have not measured memory pressure. Thousands, or even a few hundred thousand, small objects are cheap on modern runtimes. Profile first; flyweight without a measured problem is complexity for nothing.
- The language or runtime already deduplicates the value. Use string interning (`sys.intern`, `String.intern()` with care), an `enum`, or a frozen constant table instead.
- The shared part is mutable or would need per-owner changes. Copy-on-write or plain separate objects are safer.
- A columnar layout fits better. Storing a `uint16` style index per cell in a parallel array (struct of arrays) saves more memory than object references and needs no pattern vocabulary.
- You only need to cache expensive *results*. That is memoization (`functools.cache`, a `Map`), not object sharing.

## Application grill

**Inspect first:** the measured object counts and heap profile first - this pattern is for the millions, not the thousands; the state that repeats (immutable strings, geometry, icons, parsed configs); the language's built-in interning (string pools, `Symbol.for`, constant objects).

**Missing pieces:** none beyond the language: an intern map keyed by the shared state. Measure instance count and memory before and after; if the numbers are not in the millions, the map is overhead.

**Ask before you build:**

- **Scale:** what is the measured instance count and per-instance memory today?
- **Performance:** does the intern lookup (a hash on the hot path) cost more than the memory it saves?
- **Reliability:** the shared state must stay immutable - who enforces that as the code evolves?
- **Budget:** engineering time on a profile-driven optimization versus the memory it saves?
- **Implementation:** what is the key that proves two instances can share (equality semantics), and where is it documented?

## Implementation steps

1. Measure. Capture a heap profile and identify the class whose instances and duplicated fields dominate.
2. Split that class's fields into intrinsic (shared, never changes after creation) and extrinsic (per instance or computed from context).
3. Create the flyweight class with only intrinsic fields, make it immutable, and give it value-based equality.
4. Change methods that used extrinsic fields to take them as parameters.
5. Write the factory: a map keyed by the intrinsic value. Make it thread-safe if it is shared across threads.
6. Replace every `new` of the old class with a factory call, and measure again to confirm the saving.

## Example

A spreadsheet engine loads a 200,000-row export. Every cell had its own style object, and the heap profile showed styles taking more memory than the values. Only four distinct styles exist.

```ts title="cells.ts"
type NumberFormat = "plain" | "currency" | "percent";

// Flyweight: intrinsic, shared, immutable.
class CellStyle {
  constructor(
    readonly font: string,
    readonly sizePt: number,
    readonly bold: boolean,
    readonly numberFormat: NumberFormat,
  ) {
    Object.freeze(this);
  }

  // Extrinsic state (the value) is passed in, never stored.
  format(value: number): string {
    switch (this.numberFormat) {
      case "currency": return `$${value.toFixed(2)}`;
      case "percent": return `${(value * 100).toFixed(1)}%`;
      default: return String(value);
    }
  }
}

class StyleRegistry {
  private readonly pool = new Map<string, CellStyle>();

  get(font: string, sizePt: number, bold: boolean, numberFormat: NumberFormat): CellStyle {
    const key = `${font}|${sizePt}|${bold}|${numberFormat}`;
    let style = this.pool.get(key);
    if (!style) {
      style = new CellStyle(font, sizePt, bold, numberFormat);
      this.pool.set(key, style);
    }
    return style;
  }

  get size(): number {
    return this.pool.size;
  }
}

// Context: per-cell state plus a reference to a shared style.
interface Cell { row: number; col: number; value: number; style: CellStyle }

const styles = new StyleRegistry();
const sheet: Cell[] = [];
for (let row = 0; row < 200_000; row++) {
  sheet.push({ row, col: 0, value: 19.99 + row, style: styles.get("Inter", 11, false, "currency") });
  sheet.push({ row, col: 1, value: 0.08, style: styles.get("Inter", 11, false, "percent") });
  sheet.push({ row, col: 2, value: row % 7, style: styles.get("Inter", 11, row % 7 === 0, "plain") });
}

console.log(`${sheet.length} cells share ${styles.size} style objects`); // 600000 cells share 4
const cell = sheet[3];
console.log(cell.style.format(cell.value), cell.style === sheet[0].style); // $20.99 true
```

```python title="cells.py"
from dataclasses import dataclass
from typing import Literal

NumberFormat = Literal["plain", "currency", "percent"]

@dataclass(frozen=True, slots=True)
class CellStyle:
    """Flyweight: frozen, hashable, shared."""

    font: str
    size_pt: int
    bold: bool
    number_format: NumberFormat

    def format(self, value: float) -> str:
        if self.number_format == "currency":
            return f"${value:.2f}"
        if self.number_format == "percent":
            return f"{value:.1%}"
        return str(value)

class StyleRegistry:
    def __init__(self) -> None:
        self._pool: dict[CellStyle, CellStyle] = {}

    def get(self, font: str, size_pt: int, bold: bool, number_format: NumberFormat) -> CellStyle:
        candidate = CellStyle(font, size_pt, bold, number_format)
        # The frozen dataclass is its own key; the throwaway candidate is collected right away.
        return self._pool.setdefault(candidate, candidate)

    def __len__(self) -> int:
        return len(self._pool)

@dataclass(slots=True)
class Cell:
    row: int
    col: int
    value: float
    style: CellStyle

if __name__ == "__main__":
    styles = StyleRegistry()
    sheet: list[Cell] = []
    for row in range(200_000):
        sheet.append(Cell(row, 0, 19.99 + row, styles.get("Inter", 11, False, "currency")))
        sheet.append(Cell(row, 1, 0.08, styles.get("Inter", 11, False, "percent")))
        sheet.append(Cell(row, 2, row % 7, styles.get("Inter", 11, row % 7 == 0, "plain")))

    print(f"{len(sheet)} cells share {len(styles)} style objects")  # 600000 cells share 4
    cell = sheet[3]
    print(cell.style.format(cell.value), cell.style is sheet[0].style)  # $20.99 True
```

```go title="cells.go"
package main

import "fmt"

type NumberFormat uint8

const (
	Plain NumberFormat = iota
	Currency
	Percent
)

// CellStyle is the flyweight. Its fields are never modified after creation.
type CellStyle struct {
	Font   string
	SizePt int
	Bold   bool
	Format NumberFormat
}

// Render takes the extrinsic value as an argument.
func (s *CellStyle) Render(value float64) string {
	switch s.Format {
	case Currency:
		return fmt.Sprintf("$%.2f", value)
	case Percent:
		return fmt.Sprintf("%.1f%%", value*100)
	default:
		return fmt.Sprint(value)
	}
}

// StyleRegistry uses the comparable struct itself as the map key.
type StyleRegistry struct{ pool map[CellStyle]*CellStyle }

func NewStyleRegistry() *StyleRegistry {
	return &StyleRegistry{pool: map[CellStyle]*CellStyle{}}
}

func (r *StyleRegistry) Get(key CellStyle) *CellStyle {
	if s, ok := r.pool[key]; ok {
		return s
	}
	s := &key // key is a copy, safe to share
	r.pool[key] = s
	return s
}

// Cell is the context: small extrinsic fields plus one pointer.
type Cell struct {
	Row, Col int32
	Value    float64
	Style    *CellStyle
}

func main() {
	styles := NewStyleRegistry()
	sheet := make([]Cell, 0, 600_000)
	for row := int32(0); row < 200_000; row++ {
		sheet = append(sheet,
			Cell{row, 0, 19.99 + float64(row), styles.Get(CellStyle{"Inter", 11, false, Currency})},
			Cell{row, 1, 0.08, styles.Get(CellStyle{"Inter", 11, false, Percent})},
			Cell{row, 2, float64(row % 7), styles.Get(CellStyle{"Inter", 11, row%7 == 0, Plain})},
		)
	}
	fmt.Printf("%d cells share %d style objects\n", len(sheet), len(styles.pool)) // 600000 cells share 4
	c := sheet[3]
	fmt.Println(c.Style.Render(c.Value), c.Style == sheet[0].Style) // $20.99 true
}
```

```java title="Cells.java"
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Cells {
    enum NumberFormat { PLAIN, CURRENCY, PERCENT }

    /** Flyweight: a record is immutable and has value-based equals/hashCode. */
    record CellStyle(String font, int sizePt, boolean bold, NumberFormat numberFormat) {
        String format(double value) {
            return switch (numberFormat) {
                case CURRENCY -> String.format("$%.2f", value);
                case PERCENT -> String.format("%.1f%%", value * 100);
                case PLAIN -> String.valueOf(value);
            };
        }
    }

    static final class StyleRegistry {
        private final Map<CellStyle, CellStyle> pool = new HashMap<>();

        CellStyle get(String font, int sizePt, boolean bold, NumberFormat format) {
            CellStyle candidate = new CellStyle(font, sizePt, bold, format);
            return pool.computeIfAbsent(candidate, k -> k); // short-lived candidate dies young
        }

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

    /** Context: extrinsic state plus a shared reference. */
    record Cell(int row, int col, double value, CellStyle style) {}

    public static void main(String[] args) {
        StyleRegistry styles = new StyleRegistry();
        List<Cell> sheet = new ArrayList<>(600_000);
        for (int row = 0; row < 200_000; row++) {
            sheet.add(new Cell(row, 0, 19.99 + row, styles.get("Inter", 11, false, NumberFormat.CURRENCY)));
            sheet.add(new Cell(row, 1, 0.08, styles.get("Inter", 11, false, NumberFormat.PERCENT)));
            sheet.add(new Cell(row, 2, row % 7, styles.get("Inter", 11, row % 7 == 0, NumberFormat.PLAIN)));
        }
        System.out.println(sheet.size() + " cells share " + styles.size() + " style objects"); // 600000 / 4
        Cell cell = sheet.get(3);
        System.out.println(cell.style().format(cell.value()) + " " + (cell.style() == sheet.get(0).style())); // $20.99 true
    }
}
```

In Go or any language with value types, consider going one step further: store a small style index in each cell and keep the styles in a slice. That is how spreadsheet file formats store styles on disk.

## Trade-offs

**Gains**
- Memory drops from one copy per object to one copy per distinct value.
- Equality checks on flyweights can use identity, which is fast.
- Fewer allocations reduce garbage-collector pressure in managed runtimes.

**Costs**
- Immutability is mandatory. Editing a cell's style now means asking the factory for a different flyweight, not mutating a field.
- Extrinsic state must be passed around, which makes method signatures longer and code harder to follow.
- The factory's pool is a cache that never shrinks unless you add eviction, and it needs locking if shared across threads.
- The savings are invisible in code review and easy to break by a later `new CellStyle(...)`.

## Relations

- [Composite](https://aipatterns.dev/design-patterns/composite.md) trees with many identical leaves can share them as flyweights, as long as leaves hold no parent pointer.
- [Singleton](https://aipatterns.dev/design-patterns/singleton.md) has exactly one instance of a class. Flyweight has one instance per distinct intrinsic value, and many of them.
- [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) is the natural home for the "return an existing instance" logic.
- [Proxy](https://aipatterns.dev/design-patterns/proxy.md) can make a heavy object cheap to hold by deferring it. Flyweight makes many objects cheap by sharing them.
- [State](https://aipatterns.dev/design-patterns/state.md) objects with no fields are often shared as flyweights across all contexts.

## Agent checklist

- [ ] A heap profile or measurement shows the duplicated state is a real memory problem.
- [ ] The flyweight class is immutable and has value-based equality and hashing.
- [ ] Every construction goes through the factory; a search for direct constructor calls returns only the factory.
- [ ] The factory is thread-safe if used from more than one thread.
- [ ] You compared against simpler options: built-in interning, an enum, or an index into a table.
- [ ] Memory was measured again after the change and the saving is recorded.
