Design Patterns: Flyweight

7 min
Software DesignDesign PatternsOOP

Flyweight reduces memory consumption by sharing common internal state across a large number of fine-grained objects, rather than duplicating it for every instance.


Intent

Imagine a text editor with 100,000 characters. Each character has font, size, color, and position. Storing a full copy of font + size + color per character would consume enormous memory.

Most characters sharing the same font and style can share a single style object. Only position is unique to each character.


Intrinsic vs. Extrinsic State

Flyweight divides object state into two categories:

  • Intrinsic state: shared, immutable data → stored inside the Flyweight object
  • Extrinsic state: unique, per-instance data → passed in by the client at call time

Practical Example: Text Rendering Engine

TypeScript
// Intrinsic state: font and style (shared)
interface CharacterStyle {
  font: string;
  size: number;
  color: string;
  render(char: string, x: number, y: number): void;
}

class ConcreteCharacterStyle implements CharacterStyle {
  constructor(
    public font: string,
    public size: number,
    public color: string,
  ) {}

  render(char: string, x: number, y: number): void {
    console.log(`'${char}' at (${x},${y}) [${this.font} ${this.size}px ${this.color}]`);
  }
}

// Flyweight Factory — controls shared instances
class CharacterStyleFactory {
  private cache = new Map();

  getStyle(font: string, size: number, color: string): CharacterStyle {
    const key = `${font}-${size}-${color}`;
    if (!this.cache.has(key)) {
      this.cache.set(key, new ConcreteCharacterStyle(font, size, color));
      console.log(`New style created: ${key}`);
    }
    return this.cache.get(key)!;
  }

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

// Extrinsic state: character and position (unique per instance)
interface CharInstance {
  char: string;
  x: number;
  y: number;
  style: CharacterStyle;
}

const factory = new CharacterStyleFactory();
const characters: CharInstance[] = [];

// 100 characters, mostly 'Arial 14px black'
for (let i = 0; i < 100; i++) {
  characters.push({
    char: String.fromCharCode(65 + (i % 26)),
    x: i * 10, y: 0,
    style: factory.getStyle('Arial', 14, 'black'),
  });
}

// a few in red
['H', 'e', 'l', 'l', 'o'].forEach((char, i) => {
  characters.push({
    char, x: i * 20, y: 50,
    style: factory.getStyle('Georgia', 18, 'red'),
  });
});

console.log(`Total characters: ${characters.length}`); // 105
console.log(`Shared styles: ${factory.getCount()}`);   // 2

// extrinsic state passed at render time
characters.slice(0, 3).forEach(c => c.style.render(c.char, c.x, c.y));

105 characters, only 2 style objects. The factory cache does the sharing.


When to Use

Good fits

  • You need a large number of fine-grained objects with significant shared state
  • Memory usage is a measurable bottleneck

Trade-off

Flyweight trades time complexity (factory lookup) for memory. If you don't have hundreds of thousands of objects or the duplication ratio is low, the complexity isn't worth it.


Summary

Flyweight is a performance-oriented pattern, not an everyday architectural choice. Reach for it when you've profiled a real memory problem.

Common applications: particle systems in game engines, text rendering pipelines, map tiles with thousands of repeated objects.