Skip to main content

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
tip

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 data1000 copies of name, price, recipe1 shared object per type (4 total)
Per-order dataEverything duplicatedOnly customer, size, reference
Recipe strings1000 Γ— recipe list4 Γ— recipe list
Memory savingβ€”~90% for coffee-type data

.NET Real-World Usage​

  • string.Intern() β€” CLR's string interning pool is a flyweight for strings
  • char in 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.

  • 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