Flyweight Pattern
Definitionβ
The Flyweight minimizes memory usage by sharing as much data as possible with other similar objects. It separates intrinsic (shared, immutable) state from extrinsic (unique per context) state, allowing many objects to reuse the same shared data.
Coffee Shop Exampleβ
A busy Coffee Shop processes thousands of orders per day. Each order has a coffee type with shared attributes (name, base price, recipe, roast level). Instead of storing these details in every single order, we share one CoffeeType object across all orders of the same type.
Structureβ
Flyweight β Shared Intrinsic Stateβ
// Intrinsic state β shared across all orders of this coffee type
public class CoffeeTypeFlyweight
{
public string Name { get; }
public decimal BasePrice { get; }
public string RoastLevel { get; }
public IReadOnlyList<string> Recipe { get; }
public CoffeeTypeFlyweight(string name, decimal price, string roast, List<string> recipe)
{
Name = name;
BasePrice = price;
RoastLevel = roast;
Recipe = recipe.AsReadOnly();
}
public string GetDetails() =>
$"{Name} (${BasePrice:F2}, {RoastLevel}) β {string.Join(" β ", Recipe)}";
}
Flyweight Factory β Cache & Reuseβ
public class CoffeeTypeFactory
{
private readonly Dictionary<string, CoffeeTypeFlyweight> _cache = new();
public CoffeeTypeFlyweight GetCoffeeType(string name)
{
if (_cache.TryGetValue(name, out var existing))
return existing;
var flyweight = name switch
{
"Espresso" => new CoffeeTypeFlyweight("Espresso", 2.50m, "Dark",
new() { "Grind 18g beans", "Tamp", "Extract 25sec" }),
"Latte" => new CoffeeTypeFlyweight("Latte", 4.00m, "Medium",
new() { "Pull espresso shot", "Steam milk", "Pour milk" }),
"Cappuccino" => new CoffeeTypeFlyweight("Cappuccino", 3.75m, "Medium",
new() { "Pull espresso shot", "Steam milk", "Add foam" }),
"Cold Brew" => new CoffeeTypeFlyweight("Cold Brew", 3.50m, "Light",
new() { "Steep grounds 12hr", "Filter", "Serve over ice" }),
_ => throw new ArgumentException($"Unknown coffee type: {name}")
};
_cache[name] = flyweight;
return flyweight;
}
public int CacheSize => _cache.Count;
}
Context β Extrinsic State Per Orderβ
// Extrinsic state β unique to each order
public class CoffeeOrder
{
public string CustomerName { get; }
public CupSize Size { get; }
public CoffeeTypeFlyweight CoffeeType { get; } // shared reference
public decimal MilkCost { get; }
private static readonly Dictionary<CupSize, decimal> _sizeMarkup = new()
{
[CupSize.Small] = 0.0m,
[CupSize.Medium] = 0.50m,
[CupSize.Large] = 1.00m,
};
public CoffeeOrder(string customer, CupSize size, CoffeeTypeFlyweight coffeeType, decimal milkCost = 0)
{
CustomerName = customer;
Size = size;
CoffeeType = coffeeType;
MilkCost = milkCost;
}
public decimal GetTotal() =>
CoffeeType.BasePrice + _sizeMarkup[Size] + MilkCost;
public string PrintOrder() =>
$" {CustomerName}: {Size} {CoffeeType.Name} β ${GetTotal():F2}";
}
Client β Processing Ordersβ
var factory = new CoffeeTypeFactory();
// 1000 orders β but only 4 CoffeeTypeFlyweight objects in memory
var orders = new List<CoffeeOrder>
{
new("Alice", CupSize.Large, factory.GetCoffeeType("Latte"), 0.60m),
new("Bob", CupSize.Small, factory.GetCoffeeType("Espresso")),
new("Carol", CupSize.Medium, factory.GetCoffeeType("Cappuccino"), 0.50m),
new("Dave", CupSize.Large, factory.GetCoffeeType("Cold Brew")),
new("Eve", CupSize.Medium, factory.GetCoffeeType("Latte")), // reuses Alice's Latte
new("Frank", CupSize.Small, factory.GetCoffeeType("Espresso")), // reuses Bob's Espresso
};
Console.WriteLine($"Coffee types in cache: {factory.CacheSize}");
// Coffee types in cache: 4
foreach (var order in orders)
Console.WriteLine(order.PrintOrder());
// Output:
// Alice: Large Latte β $5.60
// Bob: Small Espresso β $2.50
// Carol: Medium Cappuccino β $4.75
// Dave: Large Cold Brew β $4.50
// Eve: Medium Latte β $4.50
// Frank: Small Espresso β $2.50
// Check that shared references are actually the same object
var latte1 = factory.GetCoffeeType("Latte");
var latte2 = factory.GetCoffeeType("Latte");
Console.WriteLine($"\nSame Latte object? {ReferenceEquals(latte1, latte2)}");
// Same Latte object? True
With 1000 Latte orders, there's still only one CoffeeTypeFlyweight for "Latte" in memory. The recipe, roast level, and base price are stored once and shared by all 1000 orders.
Memory Impactβ
| Without Flyweight (1000 orders) | With Flyweight | |
|---|---|---|
| Coffee data | 1000 copies of name, price, recipe | 1 shared object per type (4 total) |
| Per-order data | Everything duplicated | Only customer, size, reference |
| Recipe strings | 1000 Γ recipe list | 4 Γ recipe list |
| Memory saving | β | ~90% for coffee-type data |
.NET Real-World Usageβ
string.Intern()β CLR's string interning pool is a flyweight for stringscharin text rendering β fonts share glyph data, each character only stores position- Connection pooling β shared connections reused across requests
ImageSource/ cached images β one image shared across multiple UI controls
When to Useβ
- Your app uses a large number of similar objects
- Most state can be made extrinsic (moved outside the shared object)
- Object identity doesn't matter β many references can point to the same instance
- Memory is a bottleneck (measured, not assumed)
When NOT to Useβ
- The number of objects is small β overhead of the factory isn't worth it
- Objects don't share much common state β no savings from sharing
- Premature optimization β only apply after profiling shows memory issues
- Objects need to be mutable β flyweight intrinsic state must be immutable
Key Takeawaysβ
- Flyweight separates intrinsic (shared, immutable) from extrinsic (per-context) state
- A factory manages the cache and returns shared instances
- All flyweight objects must be immutable β or shared state gets corrupted
- It's a performance optimization, not a structural change β measure before applying
Interview Questionsβ
Q: What happens if you make flyweight state mutable?
All objects sharing that flyweight see the mutation. If Alice's Latte flyweight changes its BasePrice to $10, every other Latte order instantly costs $10. Flyweight intrinsic state must be immutable.
Q: Is string.Intern() a flyweight?
Yes. The CLR maintains a pool of unique strings. string.Intern() returns the canonical instance, so identical strings share the same memory. It's a textbook flyweight.
Q: How is Flyweight different from caching? Caching stores computed results to avoid recomputation. Flyweight shares identical state to avoid duplication. They overlap β a flyweight factory is essentially a cache of shared objects β but the intent differs: caching is about speed, flyweight is about memory.
Related Topicsβ
- Composite β tree structures that can benefit from flyweight for leaf nodes
- Factory Method β the flyweight factory uses factory method internally
- Singleton β similar "single instance" idea, but flyweight manages multiple shared types
- Memory Management β understanding GC and memory helps decide when flyweight is worthwhile