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

  • 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 Tree structs each hold a small shared_ptr (16B on x86-64) + coords.
  • Only 2 TreeType objects 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_set is 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 != &b for 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-const flyweight 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_ptr ownership or a long-lived singleton-scoped pool.
  • Garbage collection. Pool grows monotonically unless you track use-counts. Consider weak_ptr map 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