Flyweight is the one pattern in the book that’s mostly about memory. When you have a huge number of objects that carry the same data over and over, it pulls the repeated part out and shares it. This is also the one post in the series with an actual benchmark.

   
Category Structural
Smell fixed S11: Imported listings duplicate the same product specifications
Code patterns/structural/flyweight

The Scene

Early in the trading day, CornerStore bulk imports a big batch of product listings:

[009] import count=100000
  imported 100000 listings

One line in the transcript. One hundred thousand objects in memory.

I’ll be upfront: this feature exists mainly to give Flyweight a home. Most online shops don’t hold 100,000 listings in memory in a console app. But the shape of the problem, lots of objects repeating the same data, is very real: product catalogues, map tiles, characters in a text editor, sprites in a game.


The Smell

Each listing has a SKU and a price, which are unique. It also has a brand, a manufacturer and a block of specification text, which are not:

// SMELL S11: every listing carries its own copy of the brand, manufacturer and spec text
internal class Listing
{
    public string Sku { get; init; } = "";

    public string Brand { get; init; } = "";

    public string Manufacturer { get; init; } = "";

    public string SpecText { get; init; } = "";

    public decimal Price { get; init; }
}

The importer builds those three strings fresh for every single listing:

Brand = string.Concat(brand, " & Co"),
Manufacturer = string.Concat(brand, " Manufacturing Pty Ltd"),
SpecText = string.Concat("Material: ", material, "; origin: ", origin, "; care: machine wash cold"),

Here’s the thing. The importer cycles through 5 brands, 4 materials and 3 origins. That’s only 60 possible combinations. But it creates three new strings per listing, which is 300,000 strings holding just 60 different specifications.


The Idea

Flyweight splits an object’s data into two kinds:

  • Intrinsic state: the part that’s the same across many objects and never changes. Here, that’s the brand, manufacturer and spec text.
  • Extrinsic state: the part that’s unique to each object. Here, that’s the SKU and price.

The intrinsic part goes into a small shared object, the flyweight. A factory makes sure there’s only ever one flyweight per unique combination. Each listing just points to the right one.

Think of a library. A popular novel might be borrowed by hundreds of people, but the library doesn’t print a new copy of the plot for each borrower. The book’s content is shared. What’s unique to each loan is who has it and when it’s due back.


The Refactor

The shared part becomes a small, immutable class. Read-only properties matter here, because if one listing could change its shared spec, it would change it for thousands of others:

internal sealed class ProductSpec(string brand, string manufacturer, string specText)
{
    public string Brand { get; } = brand;

    public string Manufacturer { get; } = manufacturer;

    public string SpecText { get; } = specText;
}

A factory hands out one instance per brand, material and origin, creating it only the first time it’s asked:

    public ProductSpec Get(string brand, string material, string origin)
    {
        var key = (brand, material, origin);
        if (specifications.TryGetValue(key, out var specification))
        {
            return specification;
        }

And the importer asks the factory instead of building strings:

var spec = productSpecFactory.Get(brand, material, origin);

listings.Add(new Listing(spec)
{
    Sku = string.Concat(skus[i % skus.Count], "-L", i.ToString(CultureInfo.InvariantCulture)),
    Price = 10m + (i % 90),
});

100,000 listings now share 60 specifications.

classDiagram
    class Listing {
        +Sku
        +Price
        +Spec
    }
    class ProductSpec {
        +Brand
        +Manufacturer
        +SpecText
    }
    class ProductSpecFactory {
        -Dictionary~Key, ProductSpec~ specifications
        +Get(brand, material, origin) ProductSpec
    }
    class ListingImporter {
        -ProductSpecFactory productSpecFactory
        +Import(skus, count) List~Listing~
    }
    Listing --> ProductSpec : shares intrinsic state
    ListingImporter --> ProductSpecFactory : requests flyweights
    ProductSpecFactory --> ProductSpec : caches by intrinsic key

What It Cost (and What It Saved)

The code change is small: 6 files, about 73 lines added and 12 removed. The solution also includes a separate BenchmarkDotNet project that compares the old import with the new one.

Here’s what one short benchmark run measured per 100,000-listing import:

Method Allocated Ratio
Repeated metadata 42.23 MB 1.00
Shared product specs 13.98 MB 0.33

That’s about 67% less memory allocated. Treat the exact numbers as one data point, not a guarantee: they come from a short run on one machine, and results vary by hardware and runtime. But the direction is exactly what the pattern promises.

And the transcript? Still says imported 100000 listings. The golden master doesn’t see memory, which is precisely why the benchmark exists.


When Not to Use It

  • Most of the state is unique. If listings rarely share data, the factory lookup costs more than it saves.
  • The shared data changes. Flyweights must be immutable. If brand names were edited per listing, sharing them would be a bug factory.
  • You don’t have a memory problem. For a few hundred objects, Flyweight is complexity without a payoff. Measure first, like the benchmark does.

A quick .NET note: you might be tempted to use string.Intern instead. Don’t. Interned strings live for the lifetime of the process, and you’d lose the clear domain object (ProductSpec) that makes the sharing obvious to the next developer.


Easily Confused With

  • A plain cache. A flyweight factory is a kind of cache. The difference is purpose: a cache saves time by avoiding repeated work. Flyweight saves memory by sharing immutable state across many objects, and it changes the design of those objects (here, Listing no longer owns its brand).
  • Proxy. A caching proxy also sits in front of something and remembers results, which is the next pattern in the series. A proxy controls access to one real object. A flyweight is shared by many objects.

Try It Yourself

dotnet build patterns/structural/flyweight/Flyweight.slnx
dotnet test patterns/structural/flyweight/Flyweight.slnx
dotnet run --project patterns/structural/flyweight/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json

And run the benchmark yourself:

dotnet run --configuration Release --project patterns/structural/flyweight/benchmarks/CornerStore.Benchmarks -- --job Short --filter "*ListingImportBenchmarks*"

I’d love to hear what numbers you get on your machine.

The Flyweight README has the benchmark details and a sequence diagram. The full change is in DIFF.md.


Previous: Facade · Next: Proxy