Mastering Design Patterns in C# and ASP.NET Core
Part 3.6 – Flyweight Design Pattern
Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Structural Design Pattern
Difficulty: ⭐⭐⭐⭐☆ (Intermediate to Advanced)
Prerequisites: OOP, Classes, Interfaces, Collections, Caching, Memory Management
Table of Contents
Introduction
What is the Flyweight Design Pattern?
Why Does Memory Optimization Matter?
The Problem of Creating Too Many Objects
Intrinsic vs. Extrinsic State
Real-World Analogy
Flyweight Pattern Structure
UML Class Diagram
Components of the Flyweight Pattern
Complete C# Console Application
Object Sharing and Caching
ASP.NET Core Implementation
Real-World Examples
Advantages
Disadvantages
Best Practices
Common Mistakes
Flyweight vs Singleton vs Prototype
Interview Questions
Summary
1. Introduction
Modern applications can sometimes create thousands or even millions of objects during their lifetime.
For example, imagine a text editor containing a document with:
1,000,000 characters
If every character object stores its own:
Font
Font size
Color
Font style
Character formatting
then a significant amount of memory can be consumed by duplicate information.
The same problem appears in:
Game development
Document processing
Large-scale data processing
UI systems
Image processing
Caching systems
Metadata-heavy applications
The Flyweight Design Pattern addresses this problem by sharing common object state instead of storing duplicate copies.
2. What is the Flyweight Design Pattern?
Definition
The Flyweight Design Pattern is a Structural Design Pattern that minimizes memory usage by sharing objects that contain common, reusable state.
Instead of creating thousands of similar objects, the Flyweight pattern creates a smaller number of shared objects and reuses them.
Simple Concept
Without Flyweight:
Object A → Font = Arial
Object B → Font = Arial
Object C → Font = Arial
Object D → Font = Arial
The same information is duplicated.
With Flyweight:
Object A ─┐
Object B ─┼──> Shared Arial Font Object
Object C ─┤
Object D ─┘
One object is shared by many clients.
3. Why Does Memory Optimization Matter?
Memory is a finite resource.
If an application creates a huge number of duplicate objects, it can cause:
Increased memory consumption
More frequent garbage collection
Increased CPU usage
Reduced application performance
Out-of-memory problems
Increased cloud infrastructure costs
The Flyweight Pattern can reduce memory usage when many objects share identical data.
4. The Problem of Creating Too Many Objects
Consider a game containing:
100,000 Trees
Every tree might contain:
Tree Type
Tree Texture
Tree Color
Tree Model
If every tree stores its own copy of the texture and model information, memory usage becomes unnecessarily high.
Instead, we can share the common data.
Tree Factory
|
+------------+------------+
| |
Oak Tree Pine Tree
| |
Shared Texture Shared Texture
Shared Model Shared Model
Each individual tree only needs to store information that is unique to its location.
5. Intrinsic vs. Extrinsic State
Understanding intrinsic and extrinsic state is the most important concept in the Flyweight Pattern.
Intrinsic State
Intrinsic state is information that:
Is shared
Does not change frequently
Can safely be reused
Examples:
Font Name
Font Size
Font Style
Texture
Tree Type
Country Code
Product Category
Extrinsic State
Extrinsic state is information that:
Belongs to a specific object
Changes based on context
Should not be stored in the shared Flyweight
Examples:
X Coordinate
Y Coordinate
User ID
Current Position
Document Location
Order ID
Example
For a game tree:
Intrinsic State:
Tree Type = Oak
Texture = OakTexture
Color = Green
Extrinsic State:
X = 100
Y = 250
The texture and tree type can be shared.
The position must remain specific to each tree.
6. Real-World Analogy
Imagine a classroom with 40 students.
All students use:
Same school name
Same school logo
Same textbook edition
Same classroom rules
Instead of storing a separate copy of the school information for every student, that common information can be shared.
Each student only stores their individual information:
Name
Roll Number
Marks
This is similar to the Flyweight Pattern.
7. Flyweight Pattern Structure
The Flyweight Pattern typically contains:
Flyweight
Concrete Flyweight
Flyweight Factory
Client
8. UML Class Diagram
+----------------------+
| Flyweight |
+----------------------+
| + Operation(state) |
+----------^-----------+
|
+--------+--------+
| |
+----------------+ +----------------+
| ConcreteFlyweight| | ConcreteFlyweight|
+----------------+ +----------------+
| intrinsicState | | intrinsicState |
+----------------+ +----------------+
^
|
+------+------+
| Factory |
+-------------+
| - cache |
| + Get() |
+-------------+
^
|
Client
9. Components of the Flyweight Pattern
Flyweight
Defines the interface for shared objects.
public interface ITree
{
void Display(int x, int y);
}
Concrete Flyweight
Contains intrinsic state.
public class Tree : ITree
{
private readonly string _type;
private readonly string _texture;
public Tree(string type, string texture)
{
_type = type;
_texture = texture;
}
public void Display(int x, int y)
{
Console.WriteLine(
$"Tree: {_type}, Texture: {_texture}, Position: ({x}, {y})");
}
}
Notice that:
Type
Texture
are shared.
While:
X
Y
are supplied externally.
10. Complete C# Console Application
Let's create a complete example.
Tree Interface
public interface ITree
{
void Display(int x, int y);
}
Tree Flyweight
public class Tree : ITree
{
private readonly string _type;
private readonly string _texture;
public Tree(string type, string texture)
{
_type = type;
_texture = texture;
}
public void Display(int x, int y)
{
Console.WriteLine(
$"Tree Type: {_type}, " +
$"Texture: {_texture}, " +
$"Position: ({x}, {y})");
}
}
Flyweight Factory
public class TreeFactory
{
private readonly Dictionary<string, ITree> _trees = new();
public ITree GetTree(string type, string texture)
{
string key = $"{type}_{texture}";
if (!_trees.ContainsKey(key))
{
_trees[key] = new Tree(type, texture);
Console.WriteLine($"Creating new Flyweight: {key}");
}
else
{
Console.WriteLine($"Reusing Flyweight: {key}");
}
return _trees[key];
}
}
Client
class Program
{
static void Main()
{
TreeFactory factory = new TreeFactory();
ITree tree1 =
factory.GetTree("Oak", "OakTexture");
ITree tree2 =
factory.GetTree("Oak", "OakTexture");
ITree tree3 =
factory.GetTree("Pine", "PineTexture");
tree1.Display(10, 20);
tree2.Display(100, 200);
tree3.Display(300, 400);
}
}
Output
Creating new Flyweight: Oak_OakTexture
Reusing Flyweight: Oak_OakTexture
Creating new Flyweight: Pine_PineTexture
Tree Type: Oak, Texture: OakTexture, Position: (10, 20)
Tree Type: Oak, Texture: OakTexture, Position: (100, 200)
Tree Type: Pine, Texture: PineTexture, Position: (300, 400)
Although we requested the Oak flyweight twice, only one Oak object was created.
11. Object Sharing and Caching
The TreeFactory uses:
Dictionary<string, ITree>
as a cache.
Conceptually:
First request:
Oak + OakTexture
↓
Create Object
↓
Cache
Second request:
Oak + OakTexture
↓
Check Cache
↓
Reuse Object
This is the key idea behind Flyweight.
12. ASP.NET Core Implementation
The Flyweight Pattern can be useful in ASP.NET Core when an application needs to reuse expensive, immutable metadata or configuration objects.
Consider a product catalog.
Thousands of products may belong to the same category.
Instead of repeatedly creating the same category metadata, we can share it.
Product Category Interface
public interface IProductCategory
{
string Name { get; }
string Description { get; }
}
Concrete Flyweight
public class ProductCategory : IProductCategory
{
public string Name { get; }
public string Description { get; }
public ProductCategory(
string name,
string description)
{
Name = name;
Description = description;
}
}
Factory
public class ProductCategoryFactory
{
private readonly Dictionary<string, IProductCategory> _cache = new();
public IProductCategory GetCategory(string name)
{
if (!_cache.TryGetValue(name, out var category))
{
category = new ProductCategory(
name,
$"Products in {name} category");
_cache[name] = category;
}
return category;
}
}
Dependency Injection
Because the factory maintains shared state, its lifetime matters.
For an application-wide cache, one possible registration is:
builder.Services.AddSingleton<ProductCategoryFactory>();
A singleton factory means the same factory instance is reused throughout the application's lifetime.
However, in a production system, consider:
Thread safety
Cache size
Eviction
Application memory
Multi-instance deployments
Whether the objects are immutable
Controller
[ApiController]
[Route("api/products")]
public class ProductController : ControllerBase
{
private readonly ProductCategoryFactory _factory;
public ProductController(
ProductCategoryFactory factory)
{
_factory = factory;
}
[HttpGet("category/{name}")]
public IActionResult GetCategory(string name)
{
var category = _factory.GetCategory(name);
return Ok(new
{
category.Name,
category.Description
});
}
}
13. Real-World Examples
1. Text Editors
A text editor can contain millions of characters.
Instead of storing complete formatting information for every character, common formatting objects can be shared.
Character
|
+--- Position
+--- Font Reference
+--- Color Reference
2. Game Development
Games frequently contain thousands of similar objects:
Trees
Bullets
Particles
Rocks
Buildings
Characters
Textures
Shared models and textures can significantly reduce memory usage.
3. Icon Libraries
A UI may display the same icons thousands of times.
Instead of loading the same image repeatedly:
Button 1 ─┐
Button 2 ─┼──> Shared Icon
Button 3 ─┤
Button 4 ─┘
4. Country and Currency Metadata
An application may have millions of transactions but only a small number of countries and currencies.
For example:
USD
EUR
GBP
INR
JPY
AUD
Their immutable metadata can potentially be shared.
5. Product Categories
Thousands of products may reference the same:
Electronics
Books
Clothing
Furniture
category metadata.
14. Advantages
1. Reduces Memory Consumption
Shared objects eliminate duplicate intrinsic state.
2. Improves Performance
Fewer objects may mean less allocation and less garbage collection pressure.
3. Encourages Object Reuse
Expensive objects can be reused instead of recreated.
4. Centralized Object Management
A Flyweight Factory provides one place to manage shared instances.
5. Useful for Large Data Sets
The pattern becomes valuable when the application handles a very large number of similar objects.
15. Disadvantages
Increased Complexity
The design introduces:
Factory
Cache
Shared objects
Intrinsic state
Extrinsic state
For small applications, this can be unnecessary.
Thread-Safety Concerns
Shared factories and dictionaries must be designed carefully for concurrent applications.
Increased CPU Work
The application must look up objects in the cache.
State Management Can Be Difficult
Developers must carefully distinguish shared state from object-specific state.
Not Always a Performance Win
If objects are small or there are few duplicates, Flyweight can add complexity without meaningful memory savings.
16. Best Practices
1. Make Flyweight Objects Immutable
Shared objects should generally be immutable.
For example:
public sealed class Tree
{
public string Type { get; }
public string Texture { get; }
public Tree(string type, string texture)
{
Type = type;
Texture = texture;
}
}
This prevents one client from accidentally changing state used by another client.
2. Keep Intrinsic State Separate
Ask:
"Can this information be shared safely?"
If yes, it may be intrinsic state.
3. Keep Extrinsic State Outside
Position, user-specific values, request information, and other changing state should generally remain outside the Flyweight.
4. Use Appropriate Caching
For large applications, consider:
DictionaryConcurrentDictionaryMemory caching
Distributed caching
The correct option depends on the application's requirements.
5. Consider Thread Safety
This is especially important in ASP.NET Core applications because multiple requests can execute concurrently.
For example:
private readonly ConcurrentDictionary<string, ITree>
_cache = new();
can be considered when concurrent access is required.
17. Common Mistakes
Mistake 1: Sharing Mutable State
This can cause unexpected behavior.
Avoid:
Shared Object
↓
Client A changes state
↓
Client B sees changed state
Prefer immutable Flyweights.
Mistake 2: Putting Extrinsic State Inside the Flyweight
For example:
Tree.X
Tree.Y
should generally not be stored in a shared tree object if every tree has a different position.
Mistake 3: Creating a Flyweight for Every Object
If every object is unique, there is little or no sharing benefit.
Mistake 4: Ignoring Cache Growth
An unbounded cache can itself consume significant memory.
In long-running applications, consider eviction or bounded caching where appropriate.
Mistake 5: Assuming Flyweight Always Improves Performance
Flyweight is primarily useful when:
There are many objects.
Many objects share common state.
The shared state is expensive enough to justify reuse.
18. Flyweight vs Singleton vs Prototype
| Feature | Flyweight | Singleton | Prototype |
|---|---|---|---|
| Primary Goal | Reduce memory through sharing | Ensure one instance | Clone existing object |
| Object Count | Multiple shared objects | Usually one instance | Many copies |
| Uses Caching | Often | Not necessarily | Not necessarily |
| Focus | Memory optimization | Instance control | Object creation |
| State Sharing | Yes | Entire object is shared | Usually copied |
| Typical Example | Shared textures | Application configuration | Cloning templates |
Important Difference
A Flyweight is not necessarily a Singleton.
You can have:
Oak Flyweight
Pine Flyweight
Birch Flyweight
Each is shared, but there can be multiple Flyweight instances.
19. Interview Questions
1. What is the Flyweight Design Pattern?
The Flyweight Pattern is a structural pattern that reduces memory usage by sharing common object state among multiple objects.
2. What is intrinsic state?
Intrinsic state is shared, relatively stable information stored inside the Flyweight.
Examples:
Tree Type
Texture
Font
Font Size
3. What is extrinsic state?
Extrinsic state is context-specific information that is supplied by the client rather than stored in the shared Flyweight.
Examples:
Position
User ID
Request ID
Coordinates
4. What is the purpose of a Flyweight Factory?
The factory manages and reuses Flyweight objects.
It typically:
Receives a key.
Checks the cache.
Returns an existing object if found.
Creates and caches a new object if not found.
5. Is Flyweight the same as Singleton?
No.
A Singleton typically restricts a class to one shared instance.
Flyweight allows multiple shared objects, each representing a different intrinsic state.
6. When should you use Flyweight?
Use it when:
A very large number of objects are created.
Many objects have identical intrinsic state.
Memory consumption is significant.
Shared objects can safely be reused.
7. What are the disadvantages of Flyweight?
Common disadvantages include:
Increased design complexity
Cache management
Thread-safety concerns
Difficult state management
Potential performance overhead from lookups
8. Why should Flyweight objects usually be immutable?
Because the same object may be used by many clients.
If one client modifies shared state, the change could affect all other clients.
9. Where can Flyweight be used in .NET?
Potential examples include:
Shared metadata
Text rendering
Game objects
Image/icon resources
Product categories
Configuration metadata
Cached immutable reference data
10. What is the main goal of the Flyweight Pattern?
The primary goal is:
Reduce memory usage by sharing common object state rather than duplicating it.
20. Summary
The Flyweight Design Pattern is a powerful structural pattern for applications that need to manage a large number of similar objects.
The key idea is simple:
Store common data once and share it instead of storing duplicate copies.
The pattern separates object state into:
Intrinsic State
↓
Shared
Extrinsic State
↓
Context-specific
A Flyweight Factory manages the shared objects and ensures that existing instances are reused whenever possible.
In modern C# and ASP.NET Core applications, the pattern can be useful for memory-sensitive workloads involving large numbers of similar objects, such as document rendering, game objects, metadata, icons, and cached reference data.
However, Flyweight should not be introduced simply because object reuse sounds beneficial. It is most appropriate when profiling or architectural analysis indicates that object allocation and duplicated state are meaningful performance or memory concerns.
Structural Design Patterns – Progress So Far
We have now covered:
| Part | Pattern | Category |
|---|---|---|
| 3.1 | Adapter | Structural |
| 3.2 | Bridge | Structural |
| 3.3 | Composite | Structural |
| 3.4 | Decorator | Structural |
| 3.5 | Facade | Structural |
| 3.6 | Flyweight | Structural |
| 3.7 | Proxy | Structural |
Coming Up Next: Part 3.7 – Proxy Design Pattern
In the final article of the Structural Design Patterns section, we'll explore the Proxy Design Pattern, including:
What is the Proxy Pattern?
Why do we need a Proxy?
Virtual Proxy
Protection Proxy
Remote Proxy
Smart Proxy
UML Class Diagram
Complete C# Console Application
ASP.NET Core implementation
Lazy Loading
Caching
Authorization and access control
Real-world examples
Advantages and disadvantages
Best practices
Common mistakes
Proxy vs Decorator vs Adapter
Interview questions
The Proxy Pattern is particularly important for ASP.NET Core and enterprise applications, where it can be used to control access, implement lazy loading, add caching, perform authorization, and interact with remote services.
