Harness-engineering gof-flyweight-pattern

GOF Flyweight Pattern

install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/gof-flyweight-pattern" ~/.claude/skills/intense-visions-harness-engineering-gof-flyweight-pattern && rm -rf "$T"
manifest: agents/skills/claude-code/gof-flyweight-pattern/SKILL.md
source content

GOF Flyweight Pattern

Share fine-grained objects to reduce memory usage by separating intrinsic and extrinsic state.

When to Use

  • You need a very large number of similar objects that consume too much memory
  • Object state can be split into intrinsic (shared, immutable) and extrinsic (unique per instance, passed from outside)
  • You're rendering large numbers of UI elements, particles, map tiles, or text characters
  • Profiling shows object creation and GC pressure as a bottleneck

Instructions

Identify intrinsic vs. extrinsic state first — this is the critical design step:

  • Intrinsic state: Shared, immutable, independent of context (e.g., character glyph shape, sprite texture, font data)
  • Extrinsic state: Context-dependent, passed by caller (e.g., position, color tint, size)

Particle system example:

// Flyweight — intrinsic state only (shared, immutable)
class ParticleType {
  constructor(
    public readonly texture: string, // shared texture reference
    public readonly color: string, // base color
    public readonly shape: 'circle' | 'square'
  ) {}

  // Render uses extrinsic state passed from outside
  render(x: number, y: number, opacity: number, scale: number): void {
    console.log(`Draw ${this.texture} at (${x},${y}) opacity=${opacity} scale=${scale}`);
  }
}

// Flyweight factory — ensures sharing
class ParticleTypeFactory {
  private pool = new Map<string, ParticleType>();

  get(texture: string, color: string, shape: 'circle' | 'square'): ParticleType {
    const key = `${texture}:${color}:${shape}`;
    if (!this.pool.has(key)) {
      this.pool.set(key, new ParticleType(texture, color, shape));
      console.log(`Created new ParticleType for key: ${key}`);
    }
    return this.pool.get(key)!;
  }

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

// Context — stores extrinsic state + reference to flyweight
interface ParticleContext {
  x: number;
  y: number;
  opacity: number;
  scale: number;
  type: ParticleType; // reference, not copy
}

// Client manages contexts, not individual particle objects
class ParticleSystem {
  private factory = new ParticleTypeFactory();
  private particles: ParticleContext[] = [];

  emit(
    x: number,
    y: number,
    texture: string,
    color: string,
    shape: 'circle' | 'square',
    opacity = 1,
    scale = 1
  ): void {
    const type = this.factory.get(texture, color, shape); // reuse flyweight
    this.particles.push({ x, y, opacity, scale, type });
  }

  render(): void {
    for (const p of this.particles) {
      p.type.render(p.x, p.y, p.opacity, p.scale);
    }
  }

  stats(): void {
    console.log(`Particles: ${this.particles.length}, Types: ${this.factory.poolSize()}`);
  }
}

// 10,000 particles but only 3 ParticleType objects in memory
const system = new ParticleSystem();
for (let i = 0; i < 10_000; i++) {
  system.emit(Math.random() * 800, Math.random() * 600, 'spark.png', 'yellow', 'circle');
}
system.stats(); // Particles: 10000, Types: 1

String interning (built-in flyweight in JS):

// JavaScript already interns small strings — the runtime handles this
// For structured data, use a registry:
class TagRegistry {
  private tags = new Map<string, Readonly<{ name: string; color: string }>>();

  get(name: string, color: string): Readonly<{ name: string; color: string }> {
    const key = `${name}:${color}`;
    if (!this.tags.has(key)) {
      this.tags.set(key, Object.freeze({ name, color }));
    }
    return this.tags.get(key)!;
  }
}

Details

When NOT to use: If you don't have a memory problem, don't introduce this complexity. Measure first. The pattern adds code complexity (factory, split state) that hurts readability without a concrete memory/performance benefit.

Anti-patterns:

  • Storing mutable extrinsic state inside the flyweight — breaks sharing semantics
  • Flyweight factory that returns a clone instead of the shared instance — defeats the purpose
  • Premature optimization — apply only when profiling shows object proliferation as a bottleneck

Node.js relevance: JavaScript's GC handles many small objects well, but the pattern is still valuable in:

  • Game loops with thousands of entities per frame
  • Large in-memory datasets (e.g., 100k rows with repeated category metadata)
  • WebSocket servers with thousands of connections sharing config objects

Memory savings calculation:

// Without flyweight: 10,000 particles × 500 bytes (texture data) = ~5MB
// With flyweight: 10,000 contexts × 40 bytes + 1 flyweight × 500 bytes = ~400KB
// Savings: ~12× memory reduction

Source

refactoring.guru/design-patterns/flyweight

Process

  1. Read the instructions and examples in this document.
  2. Apply the patterns to your implementation, adapting to your specific context.
  3. Verify your implementation against the details and edge cases listed above.

Harness Integration

  • Type: knowledge — this skill is a reference document, not a procedural workflow.
  • No tools or state — consumed as context by other skills and agents.

Success Criteria

  • The patterns described in this document are applied correctly in the implementation.
  • Edge cases and anti-patterns listed in this document are avoided.