Learnixo

.NET & C# Development · Lesson 34 of 229

Flyweight — Share State, Save Memory

Flyweight — Share State, Save Memory

Flyweight reduces memory use by sharing as much data as possible with similar objects. It separates shared intrinsic state (stored in the flyweight) from unique extrinsic state (passed in by the client).


Intrinsic vs Extrinsic State

Intrinsic state: shared, immutable, stored in the flyweight
  - Character glyph data (shape of 'A')
  - Icon bitmap
  - CSS class definition

Extrinsic state: unique per instance, passed by caller
  - Character position on screen (x, y)
  - Icon colour or size at a specific location
  - DOM element that the CSS applies to

Without Flyweight: 100,000 characters in a document → 100,000 objects each storing the full glyph
With Flyweight:    100,000 characters → 94 objects (one per unique character), sharing glyph data

Implementation

C#
// Flyweight — stores intrinsic (shared) state
public class ProductTypeInfo
{
    public string Category    { get; init; } = "";
    public string Description { get; init; } = "";
    public string IconUrl     { get; init; } = "";
    public string[] Tags      { get; init; } = [];

    // Expensive to compute — done once and shared
    public string SearchKeywords => string.Join(" ", Tags) + " " + Category;
}

// Context — pairs flyweight with extrinsic (unique) state
public class ProductListing
{
    private readonly ProductTypeInfo _type;   // shared reference

    public int     ProductId { get; init; }   // extrinsic
    public string  Title     { get; init; } = "";   // extrinsic
    public decimal Price     { get; init; }   // extrinsic
    public int     Stock     { get; init; }   // extrinsic

    public ProductListing(ProductTypeInfo type) => _type = type;

    public string Category    => _type.Category;
    public string IconUrl     => _type.IconUrl;
    public string Keywords    => _type.SearchKeywords;
}

// Flyweight Factory — ensures shared instances
public class ProductTypeFactory
{
    private static readonly Dictionary<string, ProductTypeInfo> _cache = new();

    public static ProductTypeInfo Get(string category)
    {
        if (!_cache.TryGetValue(category, out var info))
        {
            info = CreateTypeInfo(category);
            _cache[category] = info;
        }
        return info;
    }

    private static ProductTypeInfo CreateTypeInfo(string category) => category switch
    {
        "electronics" => new ProductTypeInfo
        {
            Category = "electronics",
            Description = "Electronic devices and accessories",
            IconUrl = "/icons/electronics.svg",
            Tags = ["tech", "gadget", "device"],
        },
        "clothing" => new ProductTypeInfo
        {
            Category = "clothing",
            Description = "Apparel and fashion",
            IconUrl = "/icons/clothing.svg",
            Tags = ["fashion", "apparel", "wear"],
        },
        _ => new ProductTypeInfo { Category = category, IconUrl = "/icons/default.svg" },
    };
}

// Usage — 10,000 products share only ~20 ProductTypeInfo objects
var listings = Enumerable.Range(1, 10_000).Select(id =>
    new ProductListing(ProductTypeFactory.Get(id % 2 == 0 ? "electronics" : "clothing"))
    {
        ProductId = id,
        Title     = $"Product {id}",
        Price     = id * 9.99m,
        Stock     = id % 50,
    }
).ToList();

Console.WriteLine($"Listings: {listings.Count}");
Console.WriteLine($"Type cache: {ProductTypeFactory.Get("electronics") == ProductTypeFactory.Get("electronics")}"); // True

.NET Built-In Flyweights

C#
// string interning — identical string literals share one instance
string a = "hello";
string b = "hello";
Console.WriteLine(ReferenceEquals(a, b));   // True — interned

// Explicit intern
string c = string.Intern(new string("hello"));
Console.WriteLine(ReferenceEquals(a, c));   // True

// Enum values — same principle: each unique value is a singleton
// Boxed small integers (0-255) in .NET also cached internally

When to Use

✓ Very large number of fine-grained objects
✓ Most object state can be shared (intrinsic >> extrinsic)
✓ Memory is the bottleneck and objects are expensive to store
✓ Object identity doesn't matter — clients can share instances

✗ When most state is unique (extrinsic) — sharing saves little
✗ When the factory overhead exceeds the memory savings
✗ For small object counts — premature optimisation

Interview Answer

"Flyweight reduces memory by sharing common state across many objects, separating intrinsic state (shared, immutable — stored in the flyweight) from extrinsic state (unique per use — passed in by the caller). Classic example: a text editor with 100,000 characters doesn't store glyph data in every character object — it stores one glyph object per unique character and records only position and style per character instance. In .NET: string interning is a built-in Flyweight. In application code: icon metadata, product category definitions, CSS class objects. The Flyweight Factory is critical — it ensures shared instances are reused, not duplicated. Only apply when you have a large number of objects (thousands+) with genuinely shared state; for smaller collections, the added complexity outweighs the savings."