Flyweight

Flyweight

A printing press uses shared letter stamps instead of casting a unique metal letter for every character in a book. The "A" stamp is reused thousands of times across every page — only its position (extrinsic state) changes. The shape of the letter (intrinsic state) is shared. Without this, printing a book would require millions of unique metal pieces instead of a few dozen reusable stamps.

The Flyweight pattern reduces memory usage by sharing common state across many fine-grained objects. It splits an object’s data into intrinsic state (shared, immutable — stored in the flyweight) and extrinsic state (unique per use — passed in by the caller). A flyweight factory returns the same instance for objects with identical intrinsic state. In an e-commerce catalog, 100,000 products might share only 50 category definitions — each product stores a category ID, not a full copy of tax rates, display rules, and shipping constraints.

flowchart TD
    subgraph Shared Flyweights
        Electronics["CategoryData: Electronics"]
        Clothing["CategoryData: Clothing"]
        Food["CategoryData: Food"]
    end
    P1["Product SKU-001"] -->|categoryId| Electronics
    P2["Product SKU-002"] -->|categoryId| Electronics
    P3["Product SKU-003"] -->|categoryId| Clothing
    P4["Product SKU-004"] -->|categoryId| Food
    P5["Product SKU-005"] -->|categoryId| Electronics
    FlyweightFactory -->|returns shared instance| Electronics
    FlyweightFactory -->|returns shared instance| Clothing
    FlyweightFactory -->|returns shared instance| Food

Problem

Each of 100,000 Product instances stores its own copy of category metadata — tax rates, display rules, shipping constraints — even though thousands of products share the same category:

public class Product
{
    public Guid Id { get; set; }
    public string Sku { get; set; } = "";
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }

    // ⚠️ These fields are identical for every product in the same category
    // 100,000 products × 3 categories = 100,000 copies of the same 3 objects
    public string CategoryName { get; set; } = "";
    public decimal TaxRate { get; set; }
    public string[] DisplayRules { get; set; } = [];
    public ShippingConstraints ShippingConstraints { get; set; } = null!;
    public string[] AllowedRegions { get; set; } = [];
}

Here's what breaks when requirements change: updating the tax rate for "Electronics" requires updating 40,000 Product instances in memory. A category rule change requires a full product reload.

Solution

Extract shared category data into a flyweight. Products store only a category ID:

// Flyweight — intrinsic state (shared, immutable)
public sealed class CategoryFlyweight
{
    public string Name { get; init; } = "";
    public decimal TaxRate { get; init; }
    public IReadOnlyList<string> DisplayRules { get; init; } = [];
    public ShippingConstraints ShippingConstraints { get; init; } = null!;
    public IReadOnlyList<string> AllowedRegions { get; init; } = [];
}

// Flyweight factory — returns the same instance for the same category
public class CategoryFlyweightFactory
{
    private readonly Dictionary<string, CategoryFlyweight> _cache = new();

    public CategoryFlyweight GetOrCreate(string categoryName, Func<CategoryFlyweight> factory)
    {
        if (!_cache.TryGetValue(categoryName, out var flyweight))
        {
            flyweight = factory();
            _cache[categoryName] = flyweight;
        }
        return flyweight; // ✅ same instance returned for all products in this category
    }
}

// Product — stores only extrinsic state (unique per product) + a reference to the flyweight
public class Product
{
    public Guid Id { get; set; }
    public string Sku { get; set; } = "";
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }

    // ✅ One shared CategoryFlyweight instance per category, not per product
    public CategoryFlyweight Category { get; set; } = null!;

    // Convenience accessors — delegate to flyweight
    public decimal TaxRate => Category.TaxRate;
    public decimal PriceWithTax => Price * (1 + Category.TaxRate);
}

// Usage: 100,000 products share 3 CategoryFlyweight instances
var factory = new CategoryFlyweightFactory();
var electronicsCategory = factory.GetOrCreate("Electronics",
    () => new CategoryFlyweight { Name = "Electronics", TaxRate = 0.20m, /* ... */ });

var products = productData.Select(p => new Product
{
    Id = p.Id,
    Sku = p.Sku,
    Price = p.Price,
    Category = factory.GetOrCreate(p.CategoryName, () => LoadCategory(p.CategoryName))
}).ToList();
// ✅ Memory: 3 CategoryFlyweight objects instead of 100,000 copies

Updating the Electronics tax rate now means updating one CategoryFlyweight instance — all 40,000 electronics products reflect the change immediately.

You Already Use This

string.Intern() — the CLR string intern pool is a Flyweight factory. string.Intern("Electronics") returns the same string instance for every call with the same value. Useful when the same string appears thousands of times (e.g., category names, status codes).

ArrayPool<T> / MemoryPool<T> — rent a buffer, use it, return it to the pool. The pool shares buffer instances across operations, avoiding repeated allocations. The rented buffer is the flyweight; the data written into it is the extrinsic state.

ObjectPool<T> (ASP.NET Core) — pools expensive-to-create objects (e.g., StringBuilder, regex matchers). The pooled object is the flyweight; the content processed by it is extrinsic.

Questions

References


Whats next