Flyweight Design Pattern | System Design (LLD)
The Flyweight Design Pattern is a structural design pattern engineered specifically to optimize memory usage (RAM) when an application needs to instantiate millions of fine-grained, repetitive objects. By separating an object's state into Intrinsic (shared, constant) and Extrinsic (unique, contextual) state, Flyweight minimizes RAM overhead by sharing single instance references across thousands or millions of contextual containers.
1. Motivation & The Memory Crisis
In system design, creating millions of objects inside application memory can quickly exhaust available RAM, leading to performance degradation, heap thrashing, or application crashes (OutOfMemoryError / Segment Faults).
The Problem Scenario: Space Asteroid Game
Imagine building a 2D space shooter game where a spaceship fires lasers at oncoming asteroids.
- Depending on the game level, millions of asteroids continuously spawn and stream down the screen.
- Each naive asteroid object stores graphical textures, physical size, material, weight, color, current screen position , and velocity .
- If unique asteroid objects are allocated directly on the heap, memory usage scales linearly with object count, consuming hundreds of megabytes or gigabytes of RAM.
The Flyweight Solution
Instead of duplicating redundant fields (like size, color, texture, and material) across millions of distinct allocations:
- Identify properties that repeat across objects.
- Store shared properties once in a single cached instance called a Flyweight.
- Point millions of tiny contextual objects to that single shared Flyweight object.
2. Core Mental Model: Intrinsic vs. Extrinsic State
To implement the Flyweight pattern, an object’s complete internal state must be divided into two distinct categories:
State Comparison Matrix
3. Architecture & Structural Components
The standard Flyweight pattern consists of four core components:
- Flyweight (
AsteroidFlyweight): Interface/class containing intrinsic state and operational methods that accept extrinsic state as parameters. - Flyweight Factory (
AsteroidFactory): Manages a cache/pool (unordered_map) of Flyweight instances. Ensures Flyweights are reused when identical intrinsic parameters are requested. - Context (
AsteroidContext): Contains the unique extrinsic state and holds a pointer/reference to a sharedFlyweightobject. - Client (
SpaceGame): Instantiates contexts via the Flyweight Factory and triggers operations.
4. Vertical Data Flow & Lifecycle Walkthrough
5. Concrete Domain Example: Space Asteroid Game
Scenario Constraints
- Total Asteroid Instances: 1,000,000
- Permutations of Intrinsic Attributes (Colors, Textures, Materials, Sizes): Restricted to 3 distinct variations in the game balance (e.g., Small Red Rocky Iron, Medium Blue Metallic Stone, Large Gray Icy Ice).
- Extrinsic Attributes: Each asteroid has random unique coordinates across screen boundaries.
6. Code Evolution: Naïve Implementation (Without Flyweight)
In this approach, all intrinsic and extrinsic fields are merged into a single heavy object class. Every spawned asteroid forces a full allocation on the heap.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
// Heavy Naive Class containing ALL properties
class Asteroid {
private:
// Intrinsic Properties (Duplicated across millions of instances)
int length;
int width;
int weight;
std::string color;
std::string texture;
std::string material;
// Extrinsic Properties (Unique per asteroid)
int posX;
int posY;
int velX;
int velY;
public:
Asteroid(int l, int w, int wt, std::string c, std::string t, std::string m,
int px, int py, int vx, int vy)
: length(l), width(w), weight(wt), color(c), texture(t), material(m),
posX(px), posY(py), velX(vx), velY(vy) {}
void render() const {
std::cout << "Rendering " << color << " " << texture << " " << material
<< " Asteroid at (" << posX << ", " << posY << ") with Speed ("
<< velX << ", " << velY << ")\n";
}
// Rough calculation of heap memory occupied per instance in bytes
size_t getMemoryUsage() const {
size_t intSize = sizeof(int) * 7; // 3 intrinsic + 4 extrinsic
size_t stringSize = sizeof(std::string) * 3;
size_t approxStringData = 32 * 3; // Heap allocation inside std::string buffer
return intSize + stringSize + approxStringData;
}
};
class NaiveSpaceGame {
private:
std::vector<Asteroid> asteroids;
public:
void spawnAsteroids(int count) {
std::cout << "Spawning " << count << " asteroids (Naive Allocation)...\n";
std::vector<std::string> colors = {"Red", "Blue", "Gray"};
std::vector<std::string> textures = {"Rocky", "Metallic", "Icy"};
std::vector<std::string> materials = {"Iron", "Stone", "Ice"};
std::vector<int> sizes = {25, 35, 45};
for (int i = 0; i < count; ++i) {
int type = i % 3; // Only 3 repeating visual variations
int l = sizes[type];
int w = sizes[type];
int wt = sizes[type] * 10;
std::string c = colors[type];
std::string t = textures[type];
std::string m = materials[type];
// Extrinsic properties generated per instance
int px = 100 + (i * 50);
int py = 200 + (i * 30);
int vx = 1;
int vy = 2;
asteroids.emplace_back(l, w, wt, c, t, m, px, py, vx, vy);
}
}
void renderSample() const {
int sampleCount = std::min(5, static_cast<int>(asteroids.size()));
for (int i = 0; i < sampleCount; ++i) {
asteroids[i].render();
}
}
size_t calculateTotalMemoryUsage() const {
if (asteroids.empty()) return 0;
return asteroids.size() * asteroids[0].getMemoryUsage();
}
};
7. Code Evolution: Flyweight Pattern Implementation
Here, intrinsic properties are extracted into an immutable AsteroidFlyweight. An AsteroidFactory manages a shared pool of these objects. Millions of lightweight AsteroidContext instances hold spatial coordinates alongside a shared pointer to a cached Flyweight.
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <memory>
#include <algorithm>
// 1. FLYWEIGHT CLASS: Contains ONLY Intrinsic State
class AsteroidFlyweight {
private:
int length;
int width;
int weight;
std::string color;
std::string texture;
std::string material;
public:
AsteroidFlyweight(int l, int w, int wt, std::string c, std::string t, std::string m)
: length(l), width(w), weight(wt), color(c), texture(t), material(m) {}
// Extrinsic properties passed dynamically as method parameters
void render(int posX, int posY, int velX, int velY) const {
std::cout << "Rendering " << color << " " << texture << " " << material
<< " Asteroid at (" << posX << ", " << posY << ") with Speed ("
<< velX << ", " << velY << ")\n";
}
size_t getMemoryUsage() const {
size_t intSize = sizeof(int) * 3;
size_t stringSize = sizeof(std::string) * 3;
size_t approxStringData = 32 * 3;
return intSize + stringSize + approxStringData;
}
};
// 2. FLYWEIGHT FACTORY: Manages Caching Pool
class AsteroidFactory {
private:
static std::unordered_map<std::string, std::shared_ptr<AsteroidFlyweight>> flyweights;
public:
static std::shared_ptr<AsteroidFlyweight> getAsteroid(
int length, int width, int weight,
const std::string& color, const std::string& texture, const std::string& material)
{
// Generate composite unique string key
std::string key = std::to_string(length) + "_" + std::to_string(width) + "_" +
std::to_string(weight) + "_" + color + "_" + texture + "_" + material;
// Check cache
if (flyweights.find(key) == flyweights.end()) {
flyweights[key] = std::make_shared<AsteroidFlyweight>(length, width, weight, color, texture, material);
}
return flyweights[key];
}
static size_t getFlyweightCount() {
return flyweights.size();
}
static size_t getTotalFlyweightMemory() {
size_t total = 0;
for (const auto& pair : flyweights) {
total += pair.second->getMemoryUsage();
}
return total;
}
static void clearPool() {
flyweights.clear();
}
};
// Initialize static member
std::unordered_map<std::string, std::shared_ptr<AsteroidFlyweight>> AsteroidFactory::flyweights;
// 3. CONTEXT CLASS: Contains Extrinsic State + Shared Reference
class AsteroidContext {
private:
int posX;
int posY;
int velX;
int velY;
std::shared_ptr<AsteroidFlyweight> flyweight; // Reference to shared intrinsic state
public:
AsteroidContext(int px, int py, int vx, int vy, std::shared_ptr<AsteroidFlyweight> fw)
: posX(px), posY(py), velX(vx), velY(vy), flyweight(fw) {}
void render() const {
// Delegates rendering to Flyweight by supplying Extrinsic state
flyweight->render(posX, posY, velX, velY);
}
size_t getMemoryUsage() const {
// 4 integers + 1 shared_ptr reference overhead
return (sizeof(int) * 4) + sizeof(std::shared_ptr<AsteroidFlyweight>);
}
};
// 4. CLIENT / GAME ENGINE
class FlyweightSpaceGame {
private:
std::vector<AsteroidContext> asteroids;
public:
void spawnAsteroids(int count) {
std::cout << "Spawning " << count << " asteroids (Flyweight Pattern)...\n";
std::vector<std::string> colors = {"Red", "Blue", "Gray"};
std::vector<std::string> textures = {"Rocky", "Metallic", "Icy"};
std::vector<std::string> materials = {"Iron", "Stone", "Ice"};
std::vector<int> sizes = {25, 35, 45};
for (int i = 0; i < count; ++i) {
int type = i % 3;
int l = sizes[type];
int w = sizes[type];
int wt = sizes[type] * 10;
std::string c = colors[type];
std::string t = textures[type];
std::string m = materials[type];
// Get cached intrinsic flyweight
auto flyweight = AsteroidFactory::getAsteroid(l, w, wt, c, t, m);
// Extrinsic Spatial Properties
int px = 100 + (i * 50);
int py = 200 + (i * 30);
int vx = 1;
int vy = 2;
// Instantiate lightweight context
asteroids.emplace_back(px, py, vx, vy, flyweight);
}
}
void renderSample() const {
int sampleCount = std::min(5, static_cast<int>(asteroids.size()));
for (int i = 0; i < sampleCount; ++i) {
asteroids[i].render();
}
}
size_t calculateTotalMemoryUsage() const {
if (asteroids.empty()) return 0;
size_t contextMemory = asteroids.size() * asteroids[0].getMemoryUsage();
size_t flyweightMemory = AsteroidFactory::getTotalFlyweightMemory();
return contextMemory + flyweightMemory;
}
};
8. Quantitative Benchmark & Memory Analysis
Below is the comparative breakdown based on instantiating 1,000,000 Asteroids with 3 distinct physical/visual variants.
Empirical Metric Comparison Table
Calculations Detail:
- Naïve: .
- Flyweight: .
9. Important Pitfalls, Edge Cases & Best Practices
-
Strict Immutability Requirement:
- Intrinsic properties inside a shared
Flyweightobject must be immutable. - Pitfall: If a client modifies an intrinsic property (e.g., setting
color = "Yellow"on a shared Flyweight instance), that mutation instantly reflects across all contexts referencing that Flyweight instance, causing game-state corruption.
- Intrinsic properties inside a shared
-
Composite Key Construction:
FlyweightFactoryrelies on mapping key lookup operations. Keys should be generated consistently using unambiguous composite string delimiters (e.g.,"25_25_250_Red_Rocky_Iron").
-
Synergy with Builder Pattern:
- Multi-argument constructors for heavy flyweight objects can quickly become unwieldy (Anti-pattern: Telescoping Constructor). Combining Flyweight Factory initialization with the Builder Pattern ensures readable instantiation.
-
Thread Safety & Concurrent Pools:
- In multi-threaded game loops or high-throughput server backends, concurrent access to the
FlyweightFactorymap pool must be synchronized using mutexes, read-write locks, or thread-safe maps (ConcurrentHashMap).
- In multi-threaded game loops or high-throughput server backends, concurrent access to the
10. Real-World Applications
1. Open-World Video Games (e.g., GTA V, Cyberpunk 2077)
In massive open-world games rendering vast cities:
- Pedestrian NPCs: Thousands of citizens walk the street.
- Intrinsic (Flyweight): 3D mesh model, skeleton, texture mapping, walking animation state machine.
- Extrinsic (Context): Map location coordinates, current pathfinding waypoint, health status, AI state.
2. Desktop Rich Text Editors (e.g., MS Word, VS Code)
In a document containing characters:
- Character Glyphs:
- Intrinsic (Flyweight): Character glyph visual vector/bitmap (e.g., 'A', 'b', 'z'), font family metrics, glyph outline geometry.
- Extrinsic (Context): Line index, column position, local text selection state, syntax highlight color.
Summary and Key Takeaways
- Primary Goal: Flyweight is designed strictly to reduce RAM usage by reusing existing instance objects when dealing with massive counts of similar instances.
- State Dichotomy: Split object state cleanly into Intrinsic (shared, static, cached) and Extrinsic (unique, contextual, dynamic) states.
- Flyweight Factory Cache: Use a centralized factory containing a lookup cache (
Map<Key, Flyweight>) to enforce object reuse instead of allocating new instances. - Context Abstraction: The client holds lightweight Context instances containing extrinsic parameters along with a pointer to the shared Flyweight object.
- Immutability Is Critical: Intrinsic state stored inside Flyweight instances must be read-only to prevent unexpected state mutations across shared contexts.
- Measurable RAM Savings: Replaced heavy duplicate allocations with minimal context structures, yielding over memory reduction in high-scale scenarios (e.g., dropping from down to for 1 million items).
- Domain Indicators: Consider using Flyweight when LLD requirements mention constrained memory budgets, high instance counts, or fine-grained repetitive domain entities (e.g., game elements, text characters, UI nodes).