Flyweight
Flyweight cuts memory usage for large populations of similar objects by factoring out the data they have in common into shared instances, leaving each object to hold only what makes it unique.
Problem
A map-rendering engine needs to place a million trees across a forest layer. Each tree object naively stores its coordinates, current growth stage, plus a mesh, a texture atlas, and material settings describing what an oak or a pine looks like.
The mesh and texture data alone might be several megabytes per species, and it's identical across every tree of that species - yet if it lives inside each `Tree` instance, a million trees means a million redundant copies of the same few megabytes, quickly exceeding any reasonable memory budget and stalling the renderer.
Solution
Split each object's data along a fault line: the part that's identical across many instances (species, mesh, texture - the intrinsic state) versus the part that's unique to this one instance (position, growth stage - the extrinsic state).
Pull the intrinsic part out into a small, shared object - the flyweight - and store only one copy per distinct combination. The extrinsic part stays with the caller and gets passed into the flyweight's methods as arguments whenever behavior is needed. A factory sits in front of flyweight creation: it hands back an existing shared instance if one already matches the requested intrinsic data, and only builds a new one when it doesn't.
When to Use
- The application needs to keep an enormous number of objects alive at once, and their combined memory footprint is becoming a real constraint.
- Most of what makes those objects heavy is data that repeats identically across many of them, leaving only a small, cheap remainder that's genuinely unique per instance.
Real-World Examples
- **Browser text rendering** - a font/size/weight glyph bitmap is rasterized once and shared by every occurrence of that character on the page; each occurrence only needs its own screen position.
- **Java's `Integer.valueOf()` cache** - small boxed integers (-128 to 127) are pooled and reused rather than allocated fresh, since the same handful of values recur constantly in typical programs.
- **Game engine particle/instance systems** - thousands of bullets or foliage sprites reference one shared mesh-and-material record, while each instance keeps only its own transform and velocity.
Structure
A Flyweight bundles the shared intrinsic data and exposes methods that take extrinsic data as arguments rather than storing it. A Flyweight Factory owns the pool of flyweight instances and hands out a matching one on request, creating it only the first time it's needed. A Context object pairs a reference to some flyweight with the extrinsic data unique to that particular usage. The Client supplies extrinsic data and requests flyweights through the factory rather than instantiating them directly.
Participants
Holds only the data that's identical across a whole class of objects and exposes operations parameterized by whatever unique data the caller supplies at call time.
In the diagram: Oak, Pine.
Keeps a lookup pool keyed by intrinsic data. Given a request, it returns a matching flyweight from the pool or constructs and stores one if none exists yet.
In the diagram: Flyweight Factory.
Pairs the per-instance unique data (extrinsic state) with a pointer to the shared flyweight that supplies the common behavior and data.
Tracks or computes each object's extrinsic state and obtains the matching flyweight from the factory instead of allocating a fully self-contained object.
In the diagram: Client.
class TreeType {
constructor(name, color, texture) {
this.name = name;
this.color = color;
this.texture = texture;
}
draw(x, y) {
console.log(
`Drawing ${this.name} [${this.color}, ${this.texture}] at (${x}, ${y})`
);
}
}
class TreeTypeFactory {
static #types = new Map();
static getTreeType(name, color, texture) {
const key = `${name}_${color}_${texture}`;
if (!TreeTypeFactory.#types.has(key)) {
TreeTypeFactory.#types.set(key, new TreeType(name, color, texture));
console.log(`Factory: created new type '${name}'`);
}
return TreeTypeFactory.#types.get(key);
}
static getCount() { return TreeTypeFactory.#types.size; }
}
class Tree {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
}
draw() { this.type.draw(this.x, this.y); }
}
class Forest {
#trees = [];
plantTree(x, y, name, color, texture) {
const type = TreeTypeFactory.getTreeType(name, color, texture);
this.#trees.push(new Tree(x, y, type));
}
draw() { this.#trees.forEach(t => t.draw()); }
}
const forest = new Forest();
forest.plantTree(1, 2, 'Oak', 'green', 'rough');
forest.plantTree(5, 8, 'Oak', 'green', 'rough');
forest.plantTree(3, 4, 'Pine', 'dark-green', 'smooth');
forest.plantTree(7, 1, 'Pine', 'dark-green', 'smooth');
console.log(`Trees: 4 | Unique types: ${TreeTypeFactory.getCount()}`);
forest.draw();
Step 1 of 5
TreeType holds the shared, intrinsic state
`name`, `color`, `texture` are the data that's identical for every tree of the same kind - `draw(x, y)` takes position as a parameter instead of storing it, since position differs per tree.
Step 2 of 5
TreeTypeFactory caches TreeType instances by key
`getTreeType()` builds a key from `name_color_texture` and only calls `new TreeType(...)` if that key isn't already in the static `#types` map - otherwise it returns the existing shared instance.
Step 3 of 5
Tree stores per-instance extrinsic state plus a shared type
Each `Tree` keeps its own `x`/`y` (extrinsic, unique per tree) but only a reference to a shared `type` object (intrinsic, reused across trees) - `draw()` passes its own coordinates into the shared type's `draw()`.
Step 4 of 5
Forest requests types from the factory, never constructs them
`plantTree()` calls `TreeTypeFactory.getTreeType(...)` rather than `new TreeType(...)` directly - the factory is the only place `TreeType` objects get created.
Step 5 of 5
Four trees, only two shared TreeType instances
Two trees are planted as `'Oak', 'green', 'rough'` and two as `'Pine', 'dark-green', 'smooth'` - `TreeTypeFactory.getCount()` reports only 2 because the matching pairs reuse the same cached `TreeType`.
class TreeType {
constructor(
public readonly name: string,
public readonly color: string,
public readonly texture: string
) {}
public draw(x: number, y: number): void {
console.log(
`Drawing ${this.name} [${this.color}, ${this.texture}] at (${x}, ${y})`
);
}
}
class TreeTypeFactory {
private static types = new Map<string, TreeType>();
public static getTreeType(name: string, color: string, texture: string): TreeType {
const key = `${name}_${color}_${texture}`;
if (!TreeTypeFactory.types.has(key)) {
TreeTypeFactory.types.set(key, new TreeType(name, color, texture));
console.log(`Factory: created new type '${name}'`);
}
return TreeTypeFactory.types.get(key)!;
}
public static getCount(): number { return TreeTypeFactory.types.size; }
}
class Tree {
constructor(
private x: number,
private y: number,
private type: TreeType
) {}
public draw(): void { this.type.draw(this.x, this.y); }
}
class Forest {
private trees: Tree[] = [];
public plantTree(x: number, y: number, name: string, color: string, texture: string): void {
const type = TreeTypeFactory.getTreeType(name, color, texture);
this.trees.push(new Tree(x, y, type));
}
public draw(): void { this.trees.forEach(t => t.draw()); }
}
const forest = new Forest();
forest.plantTree(1, 2, 'Oak', 'green', 'rough');
forest.plantTree(5, 8, 'Oak', 'green', 'rough');
forest.plantTree(3, 4, 'Pine', 'dark-green', 'smooth');
forest.plantTree(7, 1, 'Pine', 'dark-green', 'smooth');
console.log(`Trees: 4 | Unique types: ${TreeTypeFactory.getCount()}`);
import java.util.*;
class TreeType {
private final String name, color, texture;
TreeType(String name, String color, String texture) {
this.name = name; this.color = color; this.texture = texture;
}
public void draw(int x, int y) {
System.out.printf("Drawing %s [%s, %s] at (%d, %d)%n", name, color, texture, x, y);
}
}
class TreeTypeFactory {
private static final Map<String, TreeType> types = new HashMap<>();
public static TreeType getTreeType(String name, String color, String texture) {
String key = name + "_" + color + "_" + texture;
if (!types.containsKey(key)) {
types.put(key, new TreeType(name, color, texture));
System.out.println("Factory: created new type '" + name + "'");
}
return types.get(key);
}
public static int getCount() { return types.size(); }
}
class Tree {
private final int x, y;
private final TreeType type;
Tree(int x, int y, TreeType type) { this.x = x; this.y = y; this.type = type; }
public void draw() { type.draw(x, y); }
}
class Forest {
private final List<Tree> trees = new ArrayList<>();
public void plantTree(int x, int y, String name, String color, String texture) {
trees.add(new Tree(x, y, TreeTypeFactory.getTreeType(name, color, texture)));
}
public void draw() { trees.forEach(Tree::draw); }
}
Forest forest = new Forest();
forest.plantTree(1, 2, "Oak", "green", "rough");
forest.plantTree(5, 8, "Oak", "green", "rough");
forest.plantTree(3, 4, "Pine", "dark-green", "smooth");
forest.plantTree(7, 1, "Pine", "dark-green", "smooth");
System.out.println("Trees: 4 | Unique types: " + TreeTypeFactory.getCount());
forest.draw();
class TreeType
{
public readonly string Name, Color, Texture;
public TreeType(string name, string color, string texture)
{ Name = name; Color = color; Texture = texture; }
public void Draw(int x, int y) =>
Console.WriteLine($"Drawing {Name} [{Color}, {Texture}] at ({x}, {y})");
}
class TreeTypeFactory
{
private static readonly Dictionary<string, TreeType> _types = new();
public static TreeType GetTreeType(string name, string color, string texture)
{
string key = $"{name}_{color}_{texture}";
if (!_types.ContainsKey(key))
{
_types[key] = new TreeType(name, color, texture);
Console.WriteLine($"Factory: created new type '{name}'");
}
return _types[key];
}
public static int GetCount() => _types.Count;
}
class Tree
{
private readonly int _x, _y;
private readonly TreeType _type;
public Tree(int x, int y, TreeType type) { _x = x; _y = y; _type = type; }
public void Draw() => _type.Draw(_x, _y);
}
class Forest
{
private readonly List<Tree> _trees = new();
public void PlantTree(int x, int y, string name, string color, string texture)
=> _trees.Add(new Tree(x, y, TreeTypeFactory.GetTreeType(name, color, texture)));
public void Draw() => _trees.ForEach(t => t.Draw());
}
var forest = new Forest();
forest.PlantTree(1, 2, "Oak", "green", "rough");
forest.PlantTree(5, 8, "Oak", "green", "rough");
forest.PlantTree(3, 4, "Pine", "dark-green", "smooth");
forest.PlantTree(7, 1, "Pine", "dark-green", "smooth");
Console.WriteLine($"Trees: 4 | Unique types: {TreeTypeFactory.GetCount()}");
forest.Draw();
from __future__ import annotations
class TreeType:
def __init__(self, name: str, color: str, texture: str) -> None:
self.name = name
self.color = color
self.texture = texture
def draw(self, x: int, y: int) -> None:
print(f'Drawing {self.name} [{self.color}, {self.texture}] at ({x}, {y})')
class TreeTypeFactory:
_types: dict[str, TreeType] = {}
@classmethod
def get_tree_type(cls, name: str, color: str, texture: str) -> TreeType:
key = f'{name}_{color}_{texture}'
if key not in cls._types:
cls._types[key] = TreeType(name, color, texture)
print(f"Factory: created new type '{name}'")
return cls._types[key]
@classmethod
def get_count(cls) -> int:
return len(cls._types)
class Tree:
def __init__(self, x: int, y: int, tree_type: TreeType) -> None:
self.x = x
self.y = y
self.type = tree_type
def draw(self) -> None:
self.type.draw(self.x, self.y)
class Forest:
def __init__(self) -> None:
self._trees: list[Tree] = []
def plant_tree(self, x: int, y: int, name: str, color: str, texture: str) -> None:
tree_type = TreeTypeFactory.get_tree_type(name, color, texture)
self._trees.append(Tree(x, y, tree_type))
def draw(self) -> None:
for tree in self._trees:
tree.draw()
forest = Forest()
forest.plant_tree(1, 2, 'Oak', 'green', 'rough')
forest.plant_tree(5, 8, 'Oak', 'green', 'rough')
forest.plant_tree(3, 4, 'Pine', 'dark-green', 'smooth')
forest.plant_tree(7, 1, 'Pine', 'dark-green', 'smooth')
print(f'Trees: 4 | Unique types: {TreeTypeFactory.get_count()}')
forest.draw()
Step 1 of 5
TreeType holds the shared, intrinsic state
`name`, `color`, `texture` are identical for every tree of the same kind - `draw(x, y)` takes the position as a parameter rather than storing it, since that part differs per tree.
Step 2 of 5
TreeTypeFactory caches TreeType instances by key
`get_tree_type()` builds a key from `name_color_texture` and only creates a new `TreeType` if that key isn't already in the class-level `_types` dict - otherwise it returns the cached one.
Step 3 of 5
Tree stores per-instance extrinsic state plus a shared type
Each `Tree` keeps its own `x`/`y` (unique per tree) but only a reference to a shared `tree_type` object - `draw()` passes its own coordinates into that shared object's `draw()`.
Step 4 of 5
Forest requests types from the factory, never constructs them
`plant_tree()` calls `TreeTypeFactory.get_tree_type(...)` instead of instantiating `TreeType` directly - the factory is the only place that creates them.
Step 5 of 5
Four trees, only two shared TreeType instances
Two trees share `'Oak', 'green', 'rough'` and two share `'Pine', 'dark-green', 'smooth'` - `TreeTypeFactory.get_count()` reports only 2 because matching pairs reuse the same cached `TreeType`.
Click "Run" to execute this code in a sandboxed frame and see console output here.
TypeScript runs as plain JavaScript here - type annotations are stripped, not type-checked.
Click "Run" to execute this code in a sandboxed frame and see console output here.
Advantages
- Memory footprint drops sharply once a large object population is dominated by a small number of distinct intrinsic-state combinations.
- Deduplicating shared state through a factory also means shared data gets initialized once, which can speed up construction of new instances.
- Keeping intrinsic state immutable and shared makes it inherently safe to read from multiple threads without extra synchronization.
Disadvantages
- You're spending CPU time to save memory - recomputing or re-passing extrinsic state on every call can outweigh the savings if objects aren't numerous enough.
- Separating state into intrinsic and extrinsic parts adds a layer of indirection that makes the code harder to follow than a plain self-contained object.
- Since flyweights are shared, mutating one accidentally affects every context referencing it - intrinsic state has to stay strictly read-only.
Question 1 of 10
What is intrinsic state versus extrinsic state in Flyweight?
Correct! Intrinsic state (species, mesh, texture) is shared and pulled into the flyweight; extrinsic state (position, growth stage) stays with the caller and is passed into flyweight methods as arguments.
Not quite. Intrinsic state (species, mesh, texture) is shared and pulled into the flyweight; extrinsic state (position, growth stage) stays with the caller and is passed into flyweight methods as arguments.
Question 2 of 10
In the million-trees example, what specifically consumes excessive memory if each Tree stores its own mesh and texture?
Correct! The mesh and texture data are identical across every tree of a species - storing it per instance means a million trees means a million redundant copies.
Not quite. The mesh and texture data are identical across every tree of a species - storing it per instance means a million trees means a million redundant copies.
Question 3 of 10
In the walkthrough's planting sequence, why does `TreeTypeFactory.getCount()` report 2 after four trees are planted?
Correct! Two trees are planted as Oak/green/rough and two as Pine/dark-green/smooth, so the matching pairs reuse the same cached TreeType and only two distinct instances ever get created.
Not quite. Two trees are planted as Oak/green/rough and two as Pine/dark-green/smooth, so the matching pairs reuse the same cached TreeType and only two distinct instances ever get created.
Question 4 of 10
In the walkthrough, how does `TreeTypeFactory.getTreeType()` decide whether to build a new `TreeType`?
Correct! `getTreeType()` builds a key from `name_color_texture` and only calls `new TreeType(...)` if that key isn't already in the cache map, otherwise it returns the existing shared instance.
Not quite. `getTreeType()` builds a key from `name_color_texture` and only calls `new TreeType(...)` if that key isn't already in the cache map, otherwise it returns the existing shared instance.
Question 5 of 10
What role does the factory play in Flyweight?
Correct! A factory sits in front of flyweight creation: it hands back an existing shared instance if one matches, and only builds a new one when it doesn't.
Not quite. A factory sits in front of flyweight creation: it hands back an existing shared instance if one matches, and only builds a new one when it doesn't.
Question 6 of 10
In the Structure section, what does the Context object do?
Correct! The Context pairs a reference to some flyweight with the extrinsic data unique to that particular usage - it's what the Tree class does by holding x/y plus a shared type reference.
Not quite. The Context pairs a reference to some flyweight with the extrinsic data unique to that particular usage - it's what the Tree class does by holding x/y plus a shared type reference.
Question 7 of 10
Why can shared intrinsic state be read from multiple threads without extra synchronization, according to the pros?
Correct! Keeping intrinsic state immutable and shared makes it inherently safe to read from multiple threads without extra synchronization, since nothing about it ever changes after creation.
Not quite. Keeping intrinsic state immutable and shared makes it inherently safe to read from multiple threads without extra synchronization, since nothing about it ever changes after creation.
Question 8 of 10
What does Flyweight optimize for, and how?
Correct! Flyweight cuts memory usage for large populations of similar objects by factoring out the data they have in common into shared instances.
Not quite. Flyweight cuts memory usage for large populations of similar objects by factoring out the data they have in common into shared instances.
Question 9 of 10
According to the cons, what tradeoff does Flyweight make to save memory?
Correct! You're spending CPU time to save memory - recomputing or re-passing extrinsic state on every call can outweigh the savings if objects aren't numerous enough.
Not quite. You're spending CPU time to save memory - recomputing or re-passing extrinsic state on every call can outweigh the savings if objects aren't numerous enough.
Question 10 of 10
How does Java's `Integer.valueOf()` cache illustrate Flyweight, per the real-world examples?
Correct! Small boxed integers (-128 to 127) are pooled and reused rather than allocated fresh, since the same handful of values recur constantly in typical programs - a textbook flyweight pool.
Not quite. Small boxed integers (-128 to 127) are pooled and reused rather than allocated fresh, since the same handful of values recur constantly in typical programs - a textbook flyweight pool.