designpattern.site

Flyweight Pattern

Use sharing to support large numbers of fine-grained objects efficiently by separating intrinsic state from extrinsic state.

Overview

Some applications need to work with enormous numbers of similar objects at the same time. A text editor displaying a document with thousands of characters, a game engine rendering tens of thousands of map tiles, or a particle system tracking millions of bullets — all of these face a common tension: the richer each object is, the faster memory runs out.

The Flyweight pattern is a structural design pattern that solves this by splitting each object's data into two parts. The part that never changes across instances is shared in a single canonical object. The part that varies per instance is passed in from outside at the moment it is needed. Thousands of individual objects end up sharing a handful of truly distinct internal structures.

The result is that you keep the clean object-oriented model your application logic expects, while the actual memory footprint shrinks dramatically.

Problem & Motivation

Imagine you are building a text editor. Every character on screen is an object with a position, a character code, and typographic properties — font family, font size, bold, italic, color. For a 100-page document you might have 500,000 character objects. If each one stores a full copy of "Arial 12pt bold black", the memory cost multiplies by 500,000.

The insight is that most of those characters share the same typographic properties. "Arial 12pt bold black" describes tens of thousands of characters, not one. Only the character code and the screen position differ between them. If you extract the shared typographic data into a single GlyphStyle object and have all the characters point to it, you go from 500,000 full objects down to a handful of GlyphStyle objects plus 500,000 lightweight position/code pairs.

This distinction between the shared part and the per-instance part is the heart of Flyweight:

  • Intrinsic state — data stored inside the flyweight object; shared and immutable (font, size, color).
  • Extrinsic state — data passed in by the caller each time an operation is performed; context-dependent (character code, x/y position).

Class Diagram

Loading diagram...

The structural points to note:

  • FlyweightFactory maintains a cache (often a plain map) keyed by intrinsic state. It returns an existing object when one already exists for that key, or creates a new one otherwise.
  • Flyweight stores only the intrinsic state. It must be immutable so that sharing is safe.
  • Client holds the extrinsic state and passes it as a parameter when calling flyweight operations. The client never stores a full character object — it stores a flyweight reference plus lightweight context data.

Implementation

The implementations below model a text editor that renders characters on screen. A GlyphFactory caches Glyph flyweights keyed by their style (font + size + bold flag). The Character struct (the client context) stores only the character code and position.

// Intrinsic state — shared, immutable
interface GlyphStyle {
  font: string;
  size: number;
  bold: boolean;
}

class Glyph {
  private readonly style: GlyphStyle;

  constructor(style: GlyphStyle) {
    this.style = style;
  }

  render(char: string, x: number, y: number): void {
    const { font, size, bold } = this.style;
    console.log(
      `Rendering '${char}' at (${x},${y}) — ${font} ${size}pt${bold ? " bold" : ""}`
    );
  }
}

// Factory that caches flyweights by their intrinsic key
class GlyphFactory {
  private cache = new Map<string, Glyph>();

  getGlyph(font: string, size: number, bold: boolean): Glyph {
    const key = `${font}|${size}|${bold}`;
    if (!this.cache.has(key)) {
      console.log(`  [factory] Creating new Glyph for key "${key}"`);
      this.cache.set(key, new Glyph({ font, size, bold }));
    }
    return this.cache.get(key)!;
  }

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

// Client context — stores extrinsic state + reference to flyweight
interface CharacterContext {
  char: string;
  x: number;
  y: number;
  glyph: Glyph;
}

// Usage
const factory = new GlyphFactory();

const document: CharacterContext[] = [
  { char: "H", x: 0,  y: 0, glyph: factory.getGlyph("Arial", 12, false) },
  { char: "e", x: 8,  y: 0, glyph: factory.getGlyph("Arial", 12, false) },
  { char: "l", x: 16, y: 0, glyph: factory.getGlyph("Arial", 12, false) },
  { char: "l", x: 24, y: 0, glyph: factory.getGlyph("Arial", 12, false) },
  { char: "o", x: 32, y: 0, glyph: factory.getGlyph("Arial", 12, false) },
  { char: "!", x: 40, y: 0, glyph: factory.getGlyph("Arial", 12, true) },
];

for (const ctx of document) {
  ctx.glyph.render(ctx.char, ctx.x, ctx.y);
}

console.log(`Glyph objects in cache: ${factory.cacheSize}`); // 2, not 6

Flyweights Must Be Immutable

Sharing only works if the shared object never changes after creation. If a flyweight's intrinsic state could be mutated by one caller, every other object sharing that flyweight would be silently affected. Enforce immutability by making intrinsic fields readonly (TypeScript), frozen=True on a dataclass (Python), or final fields via a record (Java). In Go, pass the struct by value when storing it as the key so callers cannot mutate what is in the cache.

Real-World Examples

Game map tiles — A tile-based game map might be 1,000 x 1,000 tiles, each visually described by a texture, a passability flag, and movement cost. These attributes repeat across the entire map — "grass", "water", "stone" are a handful of tile types shared by potentially millions of cells. Game engines (Unity, Godot, custom engines) use flyweight-like tile palettes exactly this way. Only the grid position of each cell is stored individually.

String interning in language runtimes — Java's String.intern() and Python's small-string interning are flyweight mechanisms baked into the runtime. When the JVM interns a string, subsequent identical string literals share a single String object from a pool rather than allocating a new one. This is the same intrinsic/extrinsic split: the character data is the intrinsic (shared) part; the variable that holds the reference is the extrinsic context.

GUI icon and image caches — UI frameworks like Qt and web browsers keep decoded image data in a shared cache. When the same PNG appears multiple times on screen (think repeated toolbar icons or repeated avatar images in a feed), the pixel data is loaded once and referenced from multiple render nodes. Each render node stores only position and size — the extrinsic state — while the shared ImageBitmap is the flyweight.

Pros and Cons

Advantages

Memory savings at scale — the benefit compounds with object count. Ten thousand objects sharing five flyweights use a fraction of the memory of ten thousand fully independent objects.

Transparent to callers — clients interact with flyweights through the same interface as any other object. The factory handles sharing invisibly; callers do not need to know they are sharing.

Cache locality — a small pool of flyweight objects is more likely to stay in CPU cache than thousands of individual objects scattered through the heap.

Works well with the Factory pattern — the FlyweightFactory naturally fits into systems that already use factory methods or abstract factories for object creation.

Disadvantages

Extrinsic state must be managed externally — clients are responsible for carrying and passing extrinsic state on every call. This shifts complexity out of the object and into the calling code, which can become verbose.

Hard to apply retroactively — splitting intrinsic and extrinsic state requires careful analysis. If the boundary is unclear, you risk sharing mutable data accidentally or duplicating data that should be shared.

Debugging is harder — because many objects share the same underlying flyweight, a log statement inside the flyweight's method does not tell you which logical object triggered it without the extrinsic context being included.

Thread safety on the factory — the cache in FlyweightFactory is shared state. In multi-threaded environments the factory's map needs to be synchronized or use a concurrent map structure.

Marginal benefit for small counts — if your application only ever creates a few hundred objects, the added indirection and code complexity of Flyweight is not worth it.

When to Use / When to Avoid

Use Flyweight when:

  • Your application creates a very large number of objects (thousands or more) that share most of their state.
  • Memory consumption is a measurable problem or a known constraint (embedded systems, mobile devices, game engines).
  • The objects can be cleanly split into intrinsic (shared, immutable) and extrinsic (context-dependent) parts.
  • Most object identity comes from extrinsic context, not from the object instance itself.

Avoid Flyweight when:

  • The number of objects is small — the overhead of the factory and the split architecture outweighs any savings.
  • The objects have mostly unique state with little to share.
  • Your language runtime already handles the sharing for you (string interning, enum instances).
  • The intrinsic/extrinsic boundary is not clear-cut — forcing an artificial split will make the code harder to follow without saving meaningful memory.
  • You need objects to be independently mutable — shared flyweights cannot hold per-instance mutable data.

Factory Method / Abstract Factory — Flyweight almost always relies on a factory to enforce cache lookup before creation. The FlyweightFactory is a specialized factory whose primary job is to prevent duplicate creation.

Singleton — A Flyweight with only one possible intrinsic state value is structurally a Singleton. The patterns differ in intent: Singleton controls instantiation count for lifecycle reasons; Flyweight controls it purely for memory efficiency.

Composite — Flyweights often appear as leaf nodes inside a Composite tree. For example, characters (flyweights) sit inside lines, which sit inside pages (composites). The two patterns complement each other.

Proxy — Like Flyweight, Proxy interposes an object between the caller and some other resource. Proxy controls access or adds behavior; Flyweight controls memory use through sharing. Both involve a level of indirection, but for different purposes.

Object Pool — Object Pool also reuses objects to reduce allocation cost, but pooled objects are checked out exclusively (one caller at a time), mutated, and returned. Flyweights are shared simultaneously by many callers and must be immutable. Use Pool when objects are expensive to create and have lifecycle; use Flyweight when many identical value-like objects exist at once.

Design Tip

Before reaching for Flyweight, measure first. Modern 64-bit systems and garbage collectors handle millions of small objects surprisingly well. Profile actual memory use with a realistic data set. If the heap pressure is real, the first question to ask is whether your language runtime already handles sharing for the data in question (string interning, integer caches, enum singletons). Apply Flyweight when profiling confirms the problem and the intrinsic/extrinsic split is natural — not as a premature optimization.

On this page