Flyweight Pattern
- Description: Share fine-grained immutable state across many objects to cut memory use — split intrinsic (shared) from extrinsic (per-instance) state and intern the intrinsic part.
- My Notion Note ID: K2C-2-11
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. The Problem
- 2. Structure
- 3. C++ Example
- 4. When to Use
- 5. Variants and Pitfalls
- 6. Related Patterns
- 7. References
1. The Problem
- Need huge numbers of similar objects → memory blows up.
- Many objects share identical attribute values → duplicating them wastes RAM.
- Examples: characters in a document (font, glyph metrics shared across millions of code points), particles in a simulation, trees in a forest, map tiles, ECS component archetypes.
Flyweight → split each object's state in two:
- Intrinsic state — value-only, shared across instances, immutable. Stored once in a pool.
- Extrinsic state — position, context, per-instance. Passed in by the client at each operation.
Net memory ≈ sizeof(Extrinsic) * N + sizeof(Intrinsic) * K where K ≪ N.
2. Structure
| Role | Responsibility |
|---|---|
| Flyweight | Interface accepting extrinsic state at every operation. |
| ConcreteFlyweight | Stores intrinsic state. Immutable. Shareable. |
| UnsharedConcreteFlyweight (optional) | Same interface, not actually shared (when sharing buys nothing). |
| FlyweightFactory | Creates & interns flyweights. Returns existing instance on key hit. |
| Client | Holds extrinsic state, gets flyweights from the factory, passes extrinsic state at call time. |
Sharing is enforced by the factory — never new a flyweight directly.
3. C++ Example
3.1 Tree-rendering forest
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <iostream>
#include <utility>
// Intrinsic state — shared
struct TreeType {
std::string species;
std::string texture; // imagine megabytes
std::string color;
void draw(int x, int y) const {
std::cout << species << " @ (" << x << "," << y << ")\n";
}
};
// FlyweightFactory — interns by composite key
class TreeTypeFactory {
public:
std::shared_ptr<const TreeType>
get(const std::string& sp, const std::string& tex, const std::string& col) {
std::string key = sp + "|" + tex + "|" + col;
auto it = pool_.find(key);
if (it != pool_.end()) return it->second;
auto tt = std::make_shared<const TreeType>(TreeType{sp, tex, col});
pool_.emplace(std::move(key), tt);
return tt;
}
std::size_t size() const { return pool_.size(); }
private:
std::unordered_map<std::string, std::shared_ptr<const TreeType>> pool_;
};
// Context — extrinsic state per instance
struct Tree {
int x, y;
std::shared_ptr<const TreeType> type; // shared
void draw() const { type->draw(x, y); }
};
class Forest {
public:
void plant(int x, int y, const std::string& sp,
const std::string& tex, const std::string& col) {
trees_.push_back({x, y, factory_.get(sp, tex, col)});
}
void render() const { for (const auto& t : trees_) t.draw(); }
void stats() const {
std::cout << trees_.size() << " trees, "
<< factory_.size() << " unique types\n";
}
private:
TreeTypeFactory factory_;
std::vector<Tree> trees_;
};
int main() {
Forest f;
for (int i = 0; i < 10'000; ++i) f.plant(i, i, "oak", "oak.png", "green");
for (int i = 0; i < 5'000; ++i) f.plant(i, i, "pine", "pine.png", "dark-green");
f.stats(); // 15000 trees, 2 unique types
}
- 15 000
Treestructs each hold a smallshared_ptr(16B on x86-64) + coords. - Only 2
TreeTypeobjects exist, regardless of how many trees.
3.2 String interning as Flyweight
#include <string>
#include <string_view>
#include <unordered_set>
class StringPool {
public:
std::string_view intern(std::string s) {
auto [it, _] = pool_.insert(std::move(s));
return *it; // stable address — node-based container
}
private:
std::unordered_set<std::string> pool_;
};
std::unordered_setis node-based → references stay valid across rehash.- All occurrences of the same identifier share one backing storage. Common in compilers, symbol tables.
4. When to Use
Use when:
- Application creates large numbers of similar objects.
- Memory cost is a problem.
- Most of each object's state can be made extrinsic without burdening callers.
- Object identity is not meaningful (clients don't depend on
&a != &bfor value-equal objects).
Avoid when:
- Object count is modest → savings don't repay the indirection complexity.
- Objects mutate — sharing a flyweight means mutating it affects all sharers (bug source).
- Intrinsic state is small relative to extrinsic → factory bookkeeping wastes more than it saves.
- Extracting extrinsic state makes the client API unpleasantly verbose.
5. Variants and Pitfalls
- Composite + Flyweight. Tree-shaped UIs/documents → leaves are flyweights (glyphs), composites hold positions and references.
- Const-by-construction. Flyweights should be immutable. Return
shared_ptr<const T>from the factory. - Thread safety. Factory access typically needs a mutex or a concurrent map; flyweights themselves, being immutable, are inherently thread-safe.
- Key choice. Composite keys can be expensive to build → consider hashing the intrinsic struct directly with a custom
hash.
Pitfalls:
- Accidental mutation. A non-
constflyweight handed out and modified affects every sharer. Hard to debug. - Identity confusion. Clients comparing addresses to test equality see surprising results — many "different" logical objects share one address.
- Lifetime of the pool. Pool must outlive every flyweight handle. Use
shared_ptrownership or a long-lived singleton-scoped pool. - Garbage collection. Pool grows monotonically unless you track use-counts. Consider
weak_ptrmap for auto-eviction. - Hash collisions on extrinsic-leaning keys. If "intrinsic" silently includes nearly-unique fields, the pool blows up and saves nothing.
5.1 Flyweight vs Object Pool
Different patterns despite both "reusing objects":
| Flyweight | Object Pool | |
|---|---|---|
| Purpose | Memory savings via deduplication | Allocation cost / construction cost reduction |
| Sharing | Many clients hold the same object simultaneously | One client uses it at a time, then returns it |
| Mutability | Immutable | Mutable; reset on return |
| Lifecycle | Effectively eternal (pool-owned) | Acquire / release per use |
| Example | Glyph in font cache | DB connection, thread, large buffer |
A flyweight is shared concurrently; a pooled object is shared serially.
6. Related Patterns
- Flyweight vs Object Pool. See § 5.1 — same vocabulary, different problem. Confusing them produces objects that mutate while shared (Flyweight broken) or objects pooled but never released (Pool broken).
- Flyweight + Composite. Leaves of a Composite are often Flyweights (glyphs, tiles, icons).
- Flyweight + Factory Method / Singleton. The FlyweightFactory is usually a singleton-scoped object so the pool is unique per process.
- Flyweight vs Cache. A cache may evict; a flyweight pool typically does not (or evicts via reference counting). A cache stores results of an expensive computation; a flyweight stores deduplicated state.
- Flyweight vs Proxy. Proxy keeps the subject's interface and controls access — usually 1-to-1. Flyweight is many-to-one sharing of intrinsic state.
- State / Strategy. State and Strategy objects, being immutable, are natural flyweights — share one instance for all clients in the same state/strategy.
7. References
- GoF — Design Patterns, Flyweight ch.
- refactoring.guru — Flyweight
- sourcemaking — Flyweight
- cppreference —
std::shared_ptr,std::unordered_map