Flyweight Pattern for Efficient Large-Scale Object Sharing
Use the Flyweight pattern to minimize memory usage by sharing as much data as possible between similar objects, essential for rendering large datasets
The Flyweight pattern minimizes memory usage by sharing as much data as possible between similar objects. When an application needs to create thousands of objects that share most of their state, Flyweight extracts the shared (intrinsic) state into a separate shared object, leaving only the unique (extrinsic) state in each instance.
When to Use This
- An application uses a large number of objects with shared state. See Singleton Pattern for managing single instances.
- Memory cost is high because of the sheer quantity of objects. See Caching Strategies for reducing redundant storage.
- Most object state can be made extrinsic and computed on the fly. See Object Pool for reusable instance patterns.
Problem
A document editor with 100,000 characters creates 100,000 Character objects. Each stores font, size, color, and glyph data — even though only 200 unique character styles exist in the document.
Solution
// flyweight/CharacterStyle.ts
interface CharacterStyle {
font: string;
size: number;
color: string;
bold: boolean;
}
class StyleFactory {
private styles = new Map<string, CharacterStyle>();
getStyle(font: string, size: number, color: string, bold: boolean): CharacterStyle {
const key = `${font}-${size}-${color}-${bold}`;
if (!this.styles.has(key)) {
this.styles.set(key, { font, size, color, bold });
}
return this.styles.get(key)!;
}
getStyleCount(): number {
return this.styles.size;
}
}
// Flyweight character with extrinsic position
class Character {
constructor(
private char: string,
private style: CharacterStyle // Shared intrinsic state
) {}
render(position: number): string {
// Extrinsic state: position passed at render time
return `<span style="font: ${this.style.size}px ${this.style.font}; color: ${this.style.color}; ${this.style.bold ? 'font-weight: bold;' : ''}" data-position="${position}">${this.char}</span>`;
}
}
// Document uses flyweights
class Document {
private characters: { char: Character; position: number }[] = [];
private styleFactory = new StyleFactory();
insert(char: string, position: number, font: string, size: number, color: string, bold: boolean): void {
const style = this.styleFactory.getStyle(font, size, color, bold);
const character = new Character(char, style);
this.characters.push({ char: character, position });
}
render(): string {
return this.characters
.map(c => c.char.render(c.position))
.join('');
}
getMemoryStats(): { characters: number; uniqueStyles: number } {
return {
characters: this.characters.length,
uniqueStyles: this.styleFactory.getStyleCount(),
};
}
}
// Usage
const doc = new Document();
// Insert 10,000 characters using only 3 unique styles
doc.insert('H', 0, 'Arial', 12, '#000', true);
doc.insert('e', 1, 'Arial', 12, '#000', true);
for (let i = 2; i < 10000; i++) {
doc.insert('x', i, 'Arial', 12, '#000', false);
}
console.log(doc.getMemoryStats());
// { characters: 10000, uniqueStyles: 2 }
Variation: Game Object Pool
// flyweight/Tree.ts
interface TreeType {
mesh: string;
barkTexture: string;
leafTexture: string;
}
class TreeTypeFactory {
private types = new Map<string, TreeType>();
getTreeType(mesh: string, bark: string, leaf: string): TreeType {
const key = `${mesh}-${bark}-${leaf}`;
if (!this.types.has(key)) {
this.types.set(key, { mesh, barkTexture: bark, leafTexture: leaf });
}
return this.types.get(key)!;
}
}
// Tree instance only stores position and type reference
class Tree {
constructor(
private x: number,
private y: number,
private type: TreeType // Shared flyweight
) {}
render(): void {
console.log(`Render ${this.type.mesh} at (${this.x}, ${this.y})`);
}
}
// Forest with thousands of trees using few types
class Forest {
private trees: Tree[] = [];
private typeFactory = new TreeTypeFactory();
plantTree(x: number, y: number, mesh: string, bark: string, leaf: string): void {
const type = this.typeFactory.getTreeType(mesh, bark, leaf);
this.trees.push(new Tree(x, y, type));
}
}
How It Works
- Flyweight stores the intrinsic (shared) state that belongs to many objects
- Context stores the extrinsic (unique) state and references a Flyweight
- Flyweight Factory creates and manages shared flyweight instances
- Client computes extrinsic state and passes it to the flyweight’s methods
Production Considerations
- Flyweights must be immutable; never modify shared state after creation
- Thread safety is required when the factory is accessed concurrently
- Consider using WeakMap for automatic garbage collection of unused flyweights
Common Mistakes
- Putting extrinsic state inside the Flyweight class, defeating the purpose
- Not using a factory, allowing duplicate flyweight instances
- Modifying shared flyweight state, corrupting all contexts using it
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
- Tests break when the design changes: favor stable contracts over internal structure.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Further Reading
- Official documentation: check the current reference for the framework or tool used.
- Related guides: explore the flyweight and structural-patterns guides for deeper coverage.
- Complementary patterns: review design patterns applicable to your technology stack.
- Public postmortems: study real incidents from teams that faced similar production issues.
Production Notes
- Deploy gradually using canary or blue-green to catch regressions early.
- Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
- Document the rollback in the runbook; test the procedure in staging at least once per quarter.
- Review structured logs with correlation IDs to trace requests end-to-end during incidents.
Key Takeaways
- Apply flyweight pattern for efficient large-scale object sharing when you need a practical solution for your use case.
- Monitor performance after implementation; measure latency, errors, and resource usage before and after.
- Check the Troubleshooting section for common failures; most have documented root causes with fixes.
- Keep dependencies updated and run tests in CI to prevent production regressions.
Advanced Topics
Scenario: Flyweight for Text Rendering
// Flyweight: share characters to reduce memory
interface CharacterFlyweight {
char: string;
font: string;
size: number;
render(x: number, y: number): string;
}
class Character implements CharacterFlyweight {
constructor(
public char: string,
public font: string,
public size: number
) {}
render(x: number, y: number): string {
return `<text x="${x}" y="${y}" font-family="${this.font}" font-size="${this.size}">${this.char}</text>`;
}
}
// Flyweight Factory: caches shared characters
class CharacterFactory {
private cache = new Map<string, CharacterFlyweight>();
getCharacter(char: string, font: string, size: number): CharacterFlyweight {
const key = `${char}|${font}|${size}`;
if (!this.cache.has(key)) {
this.cache.set(key, new Character(char, font, size));
console.log(`[FLYWEIGHT] Created: ${key}`);
}
return this.cache.get(key)!;
}
getCacheSize(): number { return this.cache.size; }
}
// Extrinsic context: position (not shared)
class TextRenderer {
constructor(private factory: CharacterFactory) {}
renderText(text: string, font: string, size: number, startX: number, y: number): string {
let x = startX;
let output = "";
for (const char of text) {
const flyweight = this.factory.getCharacter(char, font, size);
output += flyweight.render(x, y) + "\n";
x += size * 0.6; // approximate width
}
return output;
}
}
// Usage: render 10000 characters
const factory = new CharacterFactory();
const renderer = new TextRenderer(factory);
// Without flyweight: 10000 Character objects
// With flyweight: ~30 objects (unique char+font+size)
const text = "Hello world ".repeat(1000);
const svg = renderer.renderText(text, "Arial", 12, 10, 50);
console.log(`Cache size: ${factory.getCacheSize()}`); // ~30 unique
// Estimated memory
| Scenario | Objects | Memory |
|----------|---------|--------|
| Without flyweight | 12000 | 480KB |
| With flyweight | 30 | 1.2KB |
| Savings | 99.75% | |
Lessons:
- Flyweight shares intrinsic state (char, font, size)
- Extrinsic state (x, y) is not shared
- The factory caches unique flyweights
- Ideal for large quantities of similar objects
- Use in text editors, games (tiles), SVG rendering
### When NOT to use flyweight?
Do not use flyweight when there are few objects (factory overhead exceeds savings), when objects are mutable (flyweight requires immutable objects), or when each object has unique state with no repetition. The factory overhead and cache Map only pay off when there is high repetition of the same intrinsic state.
End of document. Review and update quarterly.
## Common Production Pitfalls
- Applying the pattern where no abstraction is needed, adding accidental complexity.
- Letting the pattern leak into unrelated modules and blur ownership boundaries.
- Over-engineering the first implementation instead of starting simple and measuring pain.
- Skipping contract tests, so refactors silently break consumers.
- Ignoring failure modes that the pattern does not cover.
- Using the pattern as a default instead of choosing the right tool for the current scale.
- Forgetting to document when to stop using the pattern and what replaces it.
- Missing observability around the pattern's performance and error propagation. Frequently Asked Questions
How is this different from a cache?
Flyweight is a design-level decision about object structure. A cache is an optimization for arbitrary data. Flyweights are part of the domain model.
When should I NOT use Flyweight?
When the number of shared states approaches the number of instances, or when computing extrinsic state is more expensive than storing it directly.
Related Resources
Implement Cache Invalidation Strategies
How to keep caches consistent with databases using TTL, write-through, write-behind, and event-driven invalidation patterns.
GuideWeb Performance Optimization Guide
A thorough guide to optimizing web application performance for better Core Web Vitals and user experience.
PatternBack-Pressure Pattern
Prevent upstream systems from overwhelming downstream consumers by propagating flow-control signals backward through the pipeline, ensuring stable throughput under load.
PatternBridge Pattern for Decoupling UI Components from Themes
Separate an abstraction from its implementation so both can vary independently using the Bridge pattern for pluggable UI themes and rendering engines
PatternContent Delivery Network (CDN) Pattern
Distribute static and live content through geographically dispersed edge servers to reduce latency, improve availability, and offload origin infrastructure.