Flyweight Pattern: Shared Objects for Memory Efficiency
Use the Flyweight pattern to share object state and cut memory usage. See Python, JavaScript, and Java examples with factory caching and real trade-offs.
Overview
Flyweight is a structural pattern that cuts memory usage by sharing data between similar objects. Rather than storing redundant state in every instance, you split it into intrinsic state (shared, stored inside the flyweight) and extrinsic state (unique to each context, passed in). A handful of flyweight objects then serves a large number of contexts.
When to Use
Reach for the Flyweight pattern when your application creates a huge number of objects that share the same internal state. It also fits when memory pressure is real and you can measure it, not just assume it, and when most of an object’s state can be moved out and passed in at use time. Typical examples include characters in a document, tiles in a game map, icons in a UI, or product SKUs in a catalog.
When to Avoid
Skip it when objects are few or mostly unique. The factory and lookup overhead will eat your savings. Also avoid it when objects must stay mutable per context; flyweights exist to share, so they should stay immutable. Skip it if the memory savings don’t justify the added complexity. Measure first.
Solution
The classic implementation uses a factory that caches flyweights by their intrinsic state.
Python
class TreeType:
_cache: dict = {}
def __init__(self, species: str, color: str, texture: str):
self.species = species
self.color = color
self.texture = texture
@classmethod
def get(cls, species: str, color: str, texture: str):
key = (species, color, texture)
if key not in cls._cache:
cls._cache[key] = cls(species, color, texture)
return cls._cache[key]
def render(self, x: int, y: int):
print(f"Rendering {self.species} at ({x}, {y}) "
f"with color={self.color}, texture={self.texture}")
class Tree:
def __init__(self, x: int, y: int, tree_type: TreeType):
self.x = x
self.y = y
self.tree_type = tree_type
def render(self):
self.tree_type.render(self.x, self.y)
# Usage: thousands of trees, only a few shared types
for i in range(1000):
t = Tree(i, i, TreeType.get("Oak", "green", "bark.png"))
t.render()
print(f"Unique tree types: {len(TreeType._cache)}") # 1, not 1000
JavaScript
class TreeType {
static cache = new Map();
constructor(species, color, texture) {
this.species = species;
this.color = color;
this.texture = texture;
}
static get(species, color, texture) {
const key = `${species}|${color}|${texture}`;
if (!this.cache.has(key)) {
this.cache.set(key, new TreeType(species, color, texture));
}
return this.cache.get(key);
}
render(x, y) {
console.log(`Rendering ${this.species} at (${x}, ${y}) color=${this.color}`);
}
}
class Tree {
constructor(x, y, treeType) {
this.x = x;
this.y = y;
this.treeType = treeType;
}
render() {
this.treeType.render(this.x, this.y);
}
}
for (let i = 0; i < 1000; i++) {
const t = new Tree(i, i, TreeType.get("Oak", "green", "bark.png"));
t.render();
}
console.log(`Unique tree types: ${TreeType.cache.size}`); // 1, not 1000
Java
import java.util.HashMap;
import java.util.Map;
public class TreeType {
private static final Map<String, TreeType> cache = new HashMap<>();
private final String species;
private final String color;
private final String texture;
private TreeType(String species, String color, String texture) {
this.species = species;
this.color = color;
this.texture = texture;
}
public static TreeType get(String species, String color, String texture) {
String key = species + "|" + color + "|" + texture;
return cache.computeIfAbsent(key, k -> new TreeType(species, color, texture));
}
public void render(int x, int y) {
System.out.println("Rendering " + species + " at (" + x + ", " + y + ")");
}
}
public class Tree {
private final int x, y;
private final TreeType type;
public Tree(int x, int y, TreeType type) {
this.x = x;
this.y = y;
this.type = type;
}
public void render() {
type.render(x, y);
}
}
for (int i = 0; i < 1000; i++) {
new Tree(i, i, TreeType.get("Oak", "green", "bark.png")).render();
}
System.out.println("Unique tree types: " + TreeType.cache.size());
Explanation
Flyweight splits object state into two groups. Intrinsic state — values such as species, color, and
texture — lives inside the flyweight and doubles as the cache key. Extrinsic state, such as
x and y, is context-specific and passed in when you use the flyweight.
The Flyweight Factory (TreeType.get()) owns a cache of shared flyweight instances. You don’t need a new
object for each tree. Instead, ask the factory for the right type and reuse it across many tree objects. That
keeps memory usage roughly proportional to the number of unique intrinsic states, not the number of objects.
Memory math
Let’s say you’ve got 100,000 trees across 5 species. Without Flyweight, each Tree carries its own species,
color, and texture strings — maybe 80 bytes of intrinsic state per tree, so 8 MB just for redundant data. With
Flyweight, you’ve got 5 TreeType objects (one per species) holding the intrinsic state, and 100,000 lightweight
Tree objects holding only x, y, and a reference. The intrinsic footprint drops from 8 MB to 400 bytes. The
savings scale with the number of objects, not the number of unique states.
Thread safety
The factory cache is a shared mutable resource. In Python, the dict lookup in TreeType.get() is atomic
under the GIL for simple operations, but computeIfAbsent-style patterns can still race. In Java,
HashMap.computeIfAbsent isn’t thread-safe — use ConcurrentHashMap instead. In JavaScript, the single-
threaded event loop means you rarely need locking, but Web Workers sharing a cache via SharedArrayBuffer
would need explicit synchronization. When in doubt, synchronize the factory’s get method or use a
concurrent map implementation.
When the split breaks down
Some objects resist the intrinsic/extrinsic split. If most of an object’s state is unique, moving it extrinsic just shifts the memory cost to the caller — you save nothing. If the intrinsic state changes over time (say, a tree’s color changes with seasons), you either invalidate the cache entry or accept that the flyweight is no longer purely shared. Both options add complexity. In those cases, a plain object with a pool or a simple cache often works better than a strict Flyweight.
Variants
The simple flyweight is the most common form: one shared object per unique intrinsic state. It fits character
glyphs, UI icons, and product catalog entries. An unshared flyweight isn’t cached for every instance, which
leaves room for odd edge cases. A compound flyweight combines other flyweights into a larger one,
useful for complex UI components. String interning is a built-in example. Java’s String.intern() and Python’s
automatic string interning apply the same idea to text.
Best Practices
Only apply this pattern after measuring real memory pressure. Premature optimization just adds complexity. Keep flyweights immutable so one context can’t mess up shared state for everyone. For large flyweights, use weak references or an eviction policy such as LRU or TTL so the cache can release memory. Run the profiler before and after to confirm the savings justify the extra code. Treat the factory as a cache, not a service locator, and keep its API focused on creating or retrieving flyweights.
If you’re working in a language with value types (like Rust or C# structs), check whether the compiler already inlines small objects. Sometimes what looks like a Flyweight candidate is already optimized away by the language runtime, and adding a factory on top just adds indirection.
Common Mistakes
Using flyweights when the intrinsic/extrinsic split is unclear produces fragile code. Mutable flyweights cause shared state corruption across contexts. Forgetting thread safety in the factory cache breaks concurrent access. Over-engineering the factory with complex eviction logic for small datasets is usually not worth it. Putting extrinsic state inside the flyweight defeats the whole purpose.
Summary
Flyweight splits object state into intrinsic (shared, cached) and extrinsic (per-context, passed in). A factory caches flyweight instances by their intrinsic state, so thousands of objects can share a handful of shared instances. Use it when you create huge numbers of similar objects and memory pressure is measurable. Keep flyweights immutable, use weak references or eviction policies for large caches, and profile before and after to confirm the savings. The pattern shines for game tiles, document glyphs, UI icons, and catalog entries — anywhere the same intrinsic state repeats across many contexts.
See Also
The Refactoring.Guru Flyweight page has a visual walkthrough with animations and a more detailed breakdown of the intrinsic/extrinsic split. The original GoF description in Design Patterns: Elements of Reusable Object-Oriented Software frames the pattern in the context of text editors and document glyph sharing. For a related structural pattern, the Proxy pattern controls access to objects and pairs well with Flyweight when you need lazy loading or access control on top of shared state. If you’re sharing objects to avoid allocation cost rather than memory cost, the Object Pool pattern is the better fit.
Frequently Asked Questions
Is Flyweight the same as a Singleton?
No. Singleton forces a class to have a single instance. Flyweight makes one instance for each unique combination of intrinsic state. Singleton is the extreme case where every bit of state is shared.
When should I not use Flyweight?
Avoid it when objects are few, state is mostly unique, or the memory savings don't justify the added complexity. Measure first, optimize second. Text rendering is a special case; see Flyweight for Text.
How does Flyweight differ from an Object Pool?
An Object Pool reuses instances to avoid the cost of allocation. Flyweight tries to shrink the memory footprint by sharing objects. Pool objects are usually mutable and return to the pool; flyweights share simultaneously across contexts.
Can I use Flyweight with mutable objects?
Only if the mutable part is extrinsic and kept outside the flyweight. If the shared object itself changes, every context that uses it ends up in danger. Keep the flyweight immutable.
Is Flyweight worth it for small projects?
Usually not. With only a few objects, the factory overhead can exceed the savings. Start simple and introduce Flyweight when you actually feel memory pressure.
Related Resources
Proxy Pattern
Provide a surrogate or placeholder for another object to control access to it. A structural design pattern for access control, lazy loading, and logging.
PatternSingleton Pattern: Java, Python & TypeScript Examples
Ensure a class has only one instance and provide global access to it — the creational design pattern for controlled object creation, with production-ready implementations.
PatternComposite Pattern
Compose objects into tree structures to represent part-whole hierarchies. A structural design pattern for treating individual objects and compositions uniformly.
PatternType Object Pattern
Define game object types as runtime data rather than hard-coding them as classes, enabling designers to create new entity variants without recompiling the codebase.
PatternObject Pool Pattern
Reuse expensive objects instead of creating and destroying them repeatedly. A creational pattern for managing scarce resources efficiently.
PatternFactory Pattern
Create objects without specifying the exact class to instantiate. A creational design pattern for flexible object creation.