Structural Patterns
Flyweight
Minimize memory usage by sharing as much data as possible with other similar objects. Flyweight separates intrinsic and extrinsic state.
The Flyweight Pattern is a structural design pattern that enables fitting more objects into available RAM by sharing common parts of state between multiple objects instead of keeping all of the data in each object. By dividing an object's properties into intrinsic (shared, static) and extrinsic (context-dependent, dynamic) categories, Flyweight provides massive memory savings for high-density object systems.
1. Learning Objectives
- Differentiate between Intrinsic (shared) and Extrinsic (contextual) states.
- Understand the structural role of the Flyweight Factory in managing object pools.
- Calculate concrete heap memory savings by analyzing object headers and primitive sizing.
- Recognize the immutability requirements of shared Flyweight objects.
- Implement thread-safe, pooled particle systems in Java, Python, and C++.
2. Problem & Naive Solution
Suppose you are building a 2D space shooter game. The game needs to render millions of bullet particles simultaneously on-screen. Each particle has:
- Position coordinates:
x,y(doubles). - Velocity vectors:
velocityX,velocityY(doubles). - Render assets:
color(String),spriteTexture(1MB byte array representing PNG graphic), anddamage(int).
The Naive Solution
In a naive architecture, every single bullet is modeled as a distinct object instantiating its own state:
This direct instancing model creates a major bottleneck:
- Out-Of-Memory (OOM) Crashes: Instantiating 10,000 bullets results in allocating 10GB of RAM due to duplicating the 1MB sprite texture array for each object.
- Garbage Collection Thrashing: As bullets are fired and destroyed, the JVM allocates and deallocates millions of heavy objects, causing long GC pauses.
3. Issues
Duplicating static graphics, mesh vectors, and invariant configuration metadata inside millions of dynamic coordinates wastes heap memory, limits performance, and restricts the capacity of simulations, games, or document processors.
4. Pattern Introduction & UML
The Flyweight Pattern solves this problem by separating the state into two categories:
- Intrinsic State (Shared): Properties that do not change based on context (e.g. bullet color, damage, sprite texture). This state is immutable and lives in a shared flyweight instance.
- Extrinsic State (Unique): Properties that vary with context (e.g. coordinates
x,yand velocities). This state is stored in lightweight context objects.
A BulletFactory manages a pool of these shared flyweights, ensuring that each unique combination of color and texture is instantiated only once.
UML: Game Particle Flyweight System
5. Participants
- Flyweight (
BulletType): Declares methods through which flyweights receive and act on extrinsic state. Stores intrinsic properties. - Flyweight Factory (
BulletFactory): Manages the pool of flyweight objects, ensuring that shared instances are reused. - Context (
Bullet): Holds the extrinsic state and a reference to the shared Flyweight object. - Client (
GameEngine): Coordinates the creation and lifecycle of contexts, passing extrinsic state to flyweight operations during rendering.
6. Theory (Intrinsic vs. Extrinsic State & Immutability)
The integrity of the Flyweight pattern depends on two critical rules:
- Strict Immutability: The intrinsic properties inside
BulletTypemust be final/read-only. Since thousands of bullets share a singleBulletTypeinstance, modifying its properties would instantly alter every bullet on the screen. - Comparison: - Flyweight: Focuses on sharing identical read-only objects to optimize memory consumption. - Singleton: Ensures a class has exactly one instance globally in the system. - Prototype: Clones existing configurations to create separate, mutable instances.
7. Syntax Explanation
Setting up flyweight pools in different languages:
- Java: Uses a static map (
private static final Map pool) protected by synchronized blocks to make the factory thread-safe. - Python: Leverages dictionaries to store the shared flyweight pool, overriding
__new__or class methods to intercept creation. - C++: Employs
std::shared_ptrinside pools, allowing automatic reference counting and clean deallocation when a flyweight type is no longer referenced.
8. Step-by-Step Implementation
- Step 1: Identify properties that are shared (Intrinsic) vs unique (Extrinsic).
- Step 2: Build the
Flyweightclass containing only the intrinsic state, making all fields final/read-only. - Step 3: Build the
FlyweightFactoryusing a hashmap to cache and return the shared flyweight instances. - Step 4: Create the
Contextclass containing the extrinsic state fields and a reference to the shared flyweight. - Step 5: Implement the client to retrieve flyweights from the factory and construct lightweight contexts dynamically.
9. Complete Code (Mini Project)
10. Code Walkthrough
Let's trace how the state separation reduces memory footprints:
- Sharing via Factories: The client requests a
BulletTypeby callingBulletFactory.getBulletType(). If a matching type exists in the pool, the factory returns the cached reference, preventing duplicate allocations of the 1MB texture byte array. - Lightweight Context Objects: The
Bulletobject is small. It contains only double fields for coordinates and velocity, plus a single 8-byte reference pointer to the sharedBulletTypeinstance. - Uniform delegation: When the game engine calls
draw()on theBullet, the call is delegated to the sharedBulletType, which receives the unique coordinates as method arguments.
11. Execution Flow
- Spawn Trigger: Client triggers bullet creation.
- Factory Lookup: The client requests the
BulletTypefrom the factory. - Instance Reuse: The factory returns a cached reference.
- Context Allocation: The client instantiates a small
Bulletcontext referencing the shared type. - Rendering: The engine loops over bullets, invoking
draw()and passing unique coordinates to the shared renderer.
12. Internal Working (JVM Memory Sizing Analysis)
Let's calculate the JVM memory savings for 100,000 active bullets:
- Naive Memory Sizing:
- Each
Bulletcontains coordinates (24 bytes), velocities (24 bytes), color reference (8 bytes), sprite texture array (1,000,000 bytes), and damage (4 bytes). - Total per bullet $\approx 1$ MB. - For 100,000 bullets: $100,000 \times 1\text{ MB} = 100\text{ GB}$ of JVM heap space. - Flyweight Memory Sizing:
- We pool 2 unique
BulletTypeobjects: $2 \times 1\text{ MB} = 2\text{ MB}$. - Each lightweightBulletcontext contains double coordinates and velocities (24 + 24 bytes), type reference pointer (8 bytes), plus object header overhead (16 bytes). Total per context $\approx 72\text{ bytes}$. - For 100,000 contexts: $100,000 \times 72\text{ bytes} \approx 7.2\text{ MB}$. - Total heap space $\approx 7.2\text{ MB} + 2\text{ MB} = 9.2\text{ MB}$ (a $99.99\%$ reduction in RAM).
13. Complexity Analysis
- Time Complexity: $O(1)$ constant time for factory pool lookups and delegating calls.
- Space Complexity: $O(U + C)$ where $U$ is the number of unique pooled flyweights and $C$ is the count of active contexts.
14. Best Practices
- Ensure Thread-Safety: Guard
BulletFactory.getBulletType()with double-checked locking or concurrent collections to prevent race conditions during initialization. - Enforce Read-Only Fields: Declare all flyweight fields as
final(Java) orconst(C++) to guarantee immutability.
15. Common Mistakes
- Exposing Mutator Methods: Exposing setter methods on Flyweight objects, causing side effects across shared instances.
- Memory Leaks via Cache Retention: Storing dynamic, transient objects inside static factory caches without eviction rules, preventing Garbage Collection.
16. Framework Usage
- Java String Pool: Literal strings are pooled automatically. Multiple string references pointing to the same literal share a single memory address.
- Java Integer Cache: Calling
Integer.valueOf(int)pools and returns cached instances for values between -128 and 127. - Java Thread Pool: Standard executors reuse worker threads (intrinsic workers) to execute transient runnable tasks (extrinsic payloads).
17. Interview Discussion
Answer: - Singleton restricts instantiation of a class to exactly one instance globally in the system. - Flyweight allows multiple instances of a class to exist, but pools them to share intrinsic state and optimize memory.
Answer: Use Soft/Weak reference wrappers (e.g.
WeakHashMap) in the factory pool. This allows the JVM to reclaim unused flyweight types when they are no longer referenced by active contexts.
Answer: Yes. Flyweight does not require interfaces. It focuses on the separation of intrinsic and extrinsic state, which can be implemented using standard classes and static maps.
18. Practice Exercises
- Easy: Implement a Python
CharacterTypeflyweight pooling fonts and styles for a text editor. - Medium: Design a
CoffeeOrdersystem where order types (Latte, Cappuccino) are pooled, and unique orders (table numbers) are contexts. - Hard: Build a graphics grid system. Each cell contains coordinates, cell background colors, and border widths. Pool the colors and borders to minimize memory usage for a 1,000x1,000 grid.
19. Challenge Problem
Design an Enterprise Web Analytics User Session Tracker. The application monitors millions of concurrent users. Each user session records an API key, device type, user country, location flag (intrinsic values), and event timestamps, visited URL paths, and HTTP query payloads (extrinsic values). Design this tracking service as a Flyweight system in Java, Python, or C++ and test it with 100,000 concurrent sessions, printing memory calculations.
20. Summary & Cheat Sheet
- Flyweight splits state into Intrinsic (shared, immutable) and Extrinsic (contextual).
- The Flyweight Factory manages the pool of shared instances.
- Guarantees significant memory savings in high-density object systems.
- Never expose setters on shared flyweight objects.
21. Quiz
1. What is the primary purpose of the Flyweight design pattern?
A) To simplify complex subsystems
B) To reduce memory consumption by sharing common parts of state between objects (Correct)
C) To control resource instantiation
2. Which state category is immutable and shared in the Flyweight pattern?
A) Extrinsic State
B) Intrinsic State (Correct)
C) Dynamic state
3. Why must Flyweight objects be strictly immutable?
A) To satisfy garbage collector rules
B) Because modifying a shared flyweight instance would affect all context objects referencing it (Correct)
C) To speed up compile-time checks
4. How are shared Flyweight instances accessed and reused?
A) Via global variables
B) Using a Flyweight Factory containing a pool map of instances (Correct)
C) By cloning prototype configs
5. Which of the following is a classic Flyweight example in Java's standard runtime library?
A) Java String Pool (Correct)
B) ArrayList
C) FileOutputStream
6. What is the extrinsic state of a Flyweight object?
A) The static image texture files
B) The context-dependent values (e.g. coordinates) passed to flyweight methods at runtime (Correct)
C) The unique ID stored in databases
7. What memory hazard is risked by caching flyweights without weak/soft references?
A) Stack Overflow
B) Heap Memory Leak (Correct)
C) Pointer dereference fault
8. How does Flyweight differ from Singleton?
A) Singleton restricts a class to one instance globally; Flyweight pools multiple shared instances containing varying intrinsic states (Correct)
B) Singleton uses inheritance; Flyweight uses composition
C) There is no difference
9. In C++, how are shared flyweight lifecycles typically managed inside a factory?
A) Using raw pointer arrays
B) Using std::shared_ptr pools to automate reference counted memory releases (Correct)
C) Via globally declared static buffers
10. For 1,000,000 particles, what is the best way to store extrinsic coordinates to save object overhead?
A) Inside an array of primitive doubles (e.g., flat arrays) managed by the client (Correct)
B) Inside individual wrapper class objects
C) Inside a database table query
22. Next Lesson Preview
In the next module, we will cover Behavioral Design Patterns. We will kick off the series with the Strategy Pattern to learn how to encapsulate interchangeable algorithms at runtime!
Related Topics
- AdapterConvert the interface of a class into another interface clients expect, allowing incompatible classes to work together
- FacadeProvide a unified, simplified interface to a set of interfaces in a subsystem, making the subsystem easier to use and decoupling clients.
- DecoratorAttach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.