Advanced

C-Sharp Design Patterns Flyweight

Level: Intermediate — knowledge of the C# language and the Visual Studio environment (Mac or Windows) is required.

Table of Contents

  1. Course Overview
  2. The Flyweight pattern explained
  1. Creating the Flyweight structure
  1. Intrinsic State vs. Extrinsic State
  1. The FlyweightFactory
  1. Les UnsharedConcreteFlyweights
  1. Complete source code
  1. Use cases and applications
  1. Summary and conclusions

1. Course Overview

Level: Intermediate — knowledge of the C# language and the Visual Studio environment (Mac or Windows) is required.

Topics covered

  • Define a flyweight interface
  • Create flyweight concrete classes
  • Understanding intrinsic and extrinsic state
  • Implement a factory class
  • Understand real world examples and implications

Prerequisites

  • Knowledge of C# language
  • Familiarity with Visual Studio (Mac or Windows)
  • For beginners: the C# Fundamentals course on Pluralsight can be used as a refresher

2. The Flyweight pattern explained

2.1 Categories of design patterns

Design patterns are classified into three categories, as defined by the Gang of Four book (Design Patterns: Elements of Reusable Object-Oriented Software):

CategoryDescription
CreativeConcern the creation of objects
StructuralConcern relationships between entities
BehavioralConcern algorithms and responsibilities between objects

The Flyweight pattern belongs to the structural category, because it focuses on relationships between entities. It uses sharding to efficiently support large quantities of fine-grained objects (fine-grained objects).

2.2 Definition of the Flyweight pattern

The Flyweight pattern allows sharing immutable data between objects, called intrinsic state. Any data specific to an individual object, passed from outside, is called extrinsic state.

Shared objects act as accessible, immutable templates, reusable as needed. This significantly reduces memory and storage usage.

2.3 UML Diagram

The UML diagram of the Flyweight pattern includes the following components:

┌─────────────────────────┐
│      <<interface>>      │
│      IFlyweight         │
│  + Operation(extrinsic) │
└────────────┬────────────┘
             │ implements
    ┌────────┴────────┐
    │                 │
┌───▼──────────┐  ┌───▼─────────────────┐
│ConcreteFly-  │  │UnsharedConcreteFly- │
│weight        │  │weight               │
│- intrinsic   │  │- all state          │
│  state       │  │  (no sharing)       │
└──────────────┘  └─────────────────────┘

┌─────────────────────────┐
│    FlyweightFactory     │
│ - cache: Dictionary     │
│ + GetFlyweight(key)     │
└─────────────────────────┘
         ▲
         │ uses
┌────────┴────────┐
│   Client code   │
└─────────────────┘

2.4 Pattern components

ComponentRole
Flyweight InterfaceDefines the contract that all flyweight classes must follow. Can accept and act on any past extrinsic state.
ConcreteFlyweightImplements the Flyweight interface. Stores its own intrinsic state. Any stored state must be shareable.
UnsharedConcreteFlyweightHas no shareable state, but still implements the interface. Often has child objects which are concrete flyweights.
FlyweightFactoryCreates and manages all flyweights. Determines whether the object already exists in a cache or needs to be created. Only point of access to the boss from the client code.

2.5 When to use this pattern

The Flyweight boss is not often encountered in the wild, as it requires a specialized scenario to be effective. It is applicable when:

  • A large number of objects must be created, causing a memory problem
  • Most of the state of objects can be made extrinsic
  • Many different groups of objects can be generalized into a few shared objects
  • Application does not depend on object identity (identity tests will not be able to distinguish shared references)

3. Creating the Flyweight structure

3.1 Example scenario: beverage ordering application

The example used in this course simulates a drink ordering application (like Starbucks). Without the Flyweight pattern, creating a new drink item for each order would represent a monumental memory load. With the boss:

  • Flyweight objects store and share their immutable values (intrinsic state)
  • Context data (order size) is passed from outside (extrinsic state)
  • A DrinkFactory class manages the retrieval and storage of shared concrete drinks
  • Drink raffle demonstrates unshared flyweights

3.2 The IDrinkFlyweight interface

The IDrinkFlyweight interface defines the contract for all drink flyweights.

Key design points:

  • Property Name is read-only (get only) — intrinsic state should never be changed externally
  • The Serve method accepts size as a parameter (extrinsic state), allowing sharing the intrinsic name while accommodating different sizes
// Flyweight blueprint
public interface IDrinkFlyweight
{
    // Intrinsic state - shared/readonly
    string Name { get; }

    // Extrinsic state
    void Serve(string size);
}

3.3 Flyweight concrete classes

Each drink is a concrete class that implements IDrinkFlyweight. To simplify, two drinks are created:

  • Espresso
  • BananaSmoothie

Initial structure (before adding intrinsic state):

public class Espresso : IDrinkFlyweight
{
    public string Name { get { return ""; } }  // à compléter

    public void Serve(string size)
    {
        Console.WriteLine();  // à compléter
    }
}

public class BananaSmoothie : IDrinkFlyweight
{
    public string Name { get { return ""; } }

    public void Serve(string size)
    {
        Console.WriteLine();
    }
}

4. Intrinsic State vs. Extrinsic State

4.1 Definitions and analogy

The distinction between intrinsic and extrinsic state is often obscured by inaccessible language. Here is a human analogy:

Analogy: Think about yourself as a person.

State typeHuman analogyExample of drink
Intrinsic (interior)Your name, your fundamental character — unchanging throughout your lifeDrink name, ingredient list
Extrinsic (external)Your age, height, weight — these values ​​change and depend on contextOrder Size (Small, Medium, Large)

Fundamental rule:

  • Intrinsic state is private, immutable, and stored in the flyweight object.
  • The extrinsic state is passed as a parameter to the Serve method and must never be stored in a shared flyweight.

4.2 Implementation in concrete classes

Here are the complete concrete classes with their intrinsic states:

public class Espresso : IDrinkFlyweight
{
    // État intrinsèque — privé, immuable
    private string _name;
    public string Name { get { return _name; } }
    private IEnumerable<string> _ingredients;

    public Espresso()
    {
        _name = "Espresso";
        _ingredients = new List<string>()
        {
            "Coffee Beans",
            "Hot Water"
        };
    }

    // État extrinsèque reçu en paramètre
    public void Serve(string size)
    {
        Console.WriteLine($"- {size} {_name} with {string.Join(", ", _ingredients)} coming up!");
    }
}

public class BananaSmoothie : IDrinkFlyweight
{
    private string _name;
    public string Name { get { return _name; } }
    private IEnumerable<string> _ingredients;

    public BananaSmoothie()
    {
        _name = "Banana Smoothie";
        _ingredients = new List<string>()
        {
            "Banana",
            "Whole Milk",
            "Vanilla Extract"
        };
    }

    public void Serve(string size)
    {
        Console.WriteLine($"- {size} {_name} with {string.Join(", ", _ingredients)} coming up!");
    }
}

Notes on intrinsic state initialization:

  • The state can be initialized either inline with the declaration of the variable, or in the class constructor.
  • Both approaches are valid; the constructor is often preferred for clarity.

4.3 Why never store extrinsic state in a flyweight

Storing size (extrinsic state) in a shared flyweight is a critical error often found in online examples. Here’s why:

  • If we save the size in an Espresso object shared during service, all references to this espresso object will be updated
  • Example: we serve a “Large” espresso, then a “Small” espresso. The first espresso served would be retroactively considered “Small”
  • This defeats the very purpose** of the Flyweight boss

4.4 The context class

If you need to store extrinsic state, the recommended practice is to use instances of a context class:

  • An array of context objects is stored in client code
  • These objects are mapped to the flyweight used
  • They switched to the flyweight extrinsic state method

Although this creates more objects, context objects take up much less memory than a set of objects created without the Flyweight pattern.


5. The FlyweightFactory

5.1 Role and responsibilities of the factory

The FlyweightFactory is the only access point to the Flyweight pattern from client code. She :

  • Provides an additional level of abstraction (like many creational patterns)
  • Makes client code cleaner, efficient and readable
  • Acts as a search assistant returning a drink per requested key
  • Manage a drink cache to avoid creating redundant objects

5.2 Implementation of the DrinkFactory

public class DrinkFactory
{
    // Cache des flyweights créés
    private Dictionary<string, IDrinkFlyweight> _drinkCache = 
        new Dictionary<string, IDrinkFlyweight>();
    
    // Compteur pour le débogage
    public int ObjectsCreated = 0;

    public IDrinkFlyweight GetDrink(string drinkKey)
    {
        IDrinkFlyweight drink = null;

        // Vérifier si l'objet existe déjà dans le cache
        if (_drinkCache.ContainsKey(drinkKey))
        {
            Console.WriteLine("\nReusing existing flyweight object.");
            return _drinkCache[drinkKey];  // Retourner directement — économie mémoire !
        }
        else
        {
            Console.WriteLine("\nCreating new flyweight object.");
            
            // Créer le bon type d'objet selon la clé
            switch (drinkKey)
            {
                case "Espresso":
                    drink = new Espresso();
                    break;
                case "BananaSmoothie":
                    drink = new BananaSmoothie();
                    break;
                default:
                    throw new Exception("This is not a flyweight drink object...");
            }
        }

        // Ajouter au cache et incrémenter le compteur
        _drinkCache.Add(drinkKey, drink);
        ObjectsCreated++;

        return drink;
    }

    public IDrinkFlyweight CreateGiveaway()
    {
        return new DrinkGiveaway();
    }

    public void ListDrinks()
    {
        Console.WriteLine($"\nFactory has {_drinkCache.Count} drink objects ready to use.");
        Console.WriteLine($"Number of objects created: {ObjectsCreated}");

        foreach (var drink in _drinkCache)
            Console.WriteLine($"\t{drink.Value.Name}");

        Console.WriteLine("\n");
    }
}

Important points about the factory:

  • The Dictionary<string, IDrinkFlyweight> uses the name of the drink as the key and the flyweight instance as the value
  • The cache can alternatively be initialized in the constructor or passed as an argument to the constructor — both approaches work
  • The ListDrinks method is only used for debugging to visualize the internal state of the factory

5.3 Demonstration of cache in action

Test scenario in Program.cs:

var drinkFactory = new DrinkFactory();

// Création de deux nouveaux objets flyweight (un de chaque type)
var largeEspresso = drinkFactory.GetDrink("Espresso");
largeEspresso.Serve("Large");

var mediumSmoothie = drinkFactory.GetDrink("BananaSmoothie");
mediumSmoothie.Serve("Medium");

// Réutilisation d'un flyweight existant — aucun nouvel objet créé !
var smallEspresso = drinkFactory.GetDrink("Espresso");
smallEspresso.Serve("Small");

drinkFactory.ListDrinks();

Expected output:

Creating new flyweight object.
- Large Espresso with Coffee Beans, Hot Water coming up!

Creating new flyweight object.
- Medium Banana Smoothie with Banana, Whole Milk, Vanilla Extract coming up!

Reusing existing flyweight object.
- Small Espresso with Coffee Beans, Hot Water coming up!

Factory has 2 drink objects ready to use.
Number of objects created: 2
    Espresso
    Banana Smoothie

Analysis of output:

  • Two objects have been created (an Espresso, a BananaSmoothie)
  • The small espresso was served without object creation — the existing flyweight was reused
  • The cache still contains 2 objects (the espresso was not duplicated)
  • In an application with dozens of drink options ordered thousands of times, memory savings become considerable

6. The UnsharedConcreteFlyweights

6.1 Why have unshared flyweights

A legitimate question: why have a flyweight class non-shareable if sharing is the very essence of the boss?

Qualified response:

  • Flyweight pattern makes intrinsic state sharing possible, but does not technically require it
  • There are rare cases where a concrete flyweight class must be able to act on extrinsic state while having non-shareable intrinsic state
  • This does not reduce memory usage, but allows you to handle edge cases while remaining within the framework of the Flyweight pattern

Concrete example: a drink giveaway (DrinkGiveaway) where each instance is different and not shareable, because the selected drink is random.

6.2 Implementing DrinkGiveaway

// Unshared concrete flyweight
public class DrinkGiveaway : IDrinkFlyweight
{
    // All state (intrinsic ET extrinsic peuvent être stockés car non partagé)
    public string Name { get { return _randomDrink.Name; } }
    
    // Tableau de boissons éligibles (instances de flyweights concrets)
    private IDrinkFlyweight[] _eligibleDrinks = new IDrinkFlyweight[]
    {
        new Espresso(),
        new BananaSmoothie()
    };

    private IDrinkFlyweight _randomDrink;
    private string _size;  // État extrinsèque stocké (permis car non partagé !)

    public DrinkGiveaway()
    {
        // Sélection aléatoire d'une boisson
        var randomIndex = new Random().Next(0, 2);
        _randomDrink = _eligibleDrinks[randomIndex];
    }

    // Extrinsic state
    public void Serve(string size)
    {
        _size = size;
        Console.WriteLine($"Free Giveaway!");
        Console.WriteLine($"- {_size} {_randomDrink.Name} coming up!");
    }
}

Key differences with a shared ConcreteFlyweight:

AppearanceConcreteFlyweight (shared)UnsharedConcreteFlyweight
Intrinsic stateShared between all instancesUnshared, unique per instance
Extrinsic statePassed as parameter onlyCan be stored in object
CreationOnly one instance per type (via factory)New instance on each request
Memory savingYesNo
Access via factoryYes (GetDrink)Yes (CreateGiveaway) but always creates a new instance

Design Note:

If you are using non-shared classes, consider removing intrinsic get properties from your flyweight interface. Non-shared classes don’t need to implement them, which will keep the code cleaner and more readable.

6.3 Integration into the factory

The CreateGiveaway method in DrinkFactory always returns a new instance of DrinkGiveaway, unlike GetDrink which reuses the cache:

public IDrinkFlyweight CreateGiveaway()
{
    return new DrinkGiveaway();  // Toujours une nouvelle instance — non partagée !
}

Test in Program.cs:

var drinkFactory = new DrinkFactory();

var sizes = new string[] { "Small", "Medium", "Large" };
foreach (var size in sizes)
{
    var giveaway = drinkFactory.CreateGiveaway();
    giveaway.Serve(size);
}

Output (varies depending on random selection):

Free Giveaway!
- Small Espresso coming up!
Free Giveaway!
- Medium Espresso coming up!
Free Giveaway!
- Large Banana Smoothie coming up!

Each of these objects is a separate instance, and storing their extrinsic state (size) is not a problem since they are not shared.


7. Full source code

7.1 Drinks.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace Flyweight_Pattern
{
    // Flyweight blueprint
    public interface IDrinkFlyweight
    {
        // Intrinsic state - shared/readonly
        string Name { get; }

        // Extrinsic state
        void Serve(string size);
    }

    public class Espresso : IDrinkFlyweight
    {
        private string _name;
        public string Name { get { return _name; } }
        private IEnumerable<string> _ingredients;

        public Espresso()
        {
            _name = "Espresso";
            _ingredients = new List<string>()
            {
                "Coffee Beans",
                "Hot Water"
            };
        }

        public void Serve(string size)
        {
            Console.WriteLine($"- {size} {_name} with {string.Join(", ", _ingredients)} coming up!");
        }
    }

    public class BananaSmoothie : IDrinkFlyweight
    {
        private string _name;
        public string Name { get { return _name; } }
        private IEnumerable<string> _ingredients;

        public BananaSmoothie()
        {
            _name = "Banana Smoothie";
            _ingredients = new List<string>()
            {
                "Banana",
                "Whole Milk",
                "Vanilla Extract"
            };
        }

        public void Serve(string size)
        {
            Console.WriteLine($"- {size} {_name} with {string.Join(", ", _ingredients)} coming up!");
        }
    }

    // Unshared concrete flyweight
    public class DrinkGiveaway : IDrinkFlyweight
    {
        // All state
        public string Name { get { return _randomDrink.Name; } }
        private IDrinkFlyweight[] _eligibleDrinks = new IDrinkFlyweight[]
        {
            new Espresso(),
            new BananaSmoothie()
        };

        private IDrinkFlyweight _randomDrink;
        private string _size;

        public DrinkGiveaway()
        {
            var randomIndex = new Random().Next(0, 2);
            _randomDrink = _eligibleDrinks[randomIndex];
        }

        // Extrinsic state
        public void Serve(string size)
        {
            _size = size;
            Console.WriteLine($"Free Giveaway!");
            Console.WriteLine($"- {_size} {_randomDrink.Name} coming up!");
        }
    }
}

7.2 Factory.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace Flyweight_Pattern
{
    public class DrinkFactory
    {
        private Dictionary<string, IDrinkFlyweight> _drinkCache = 
            new Dictionary<string, IDrinkFlyweight>();
        public int ObjectsCreated = 0;

        public IDrinkFlyweight GetDrink(string drinkKey)
        {
            IDrinkFlyweight drink = null;

            if (_drinkCache.ContainsKey(drinkKey))
            {
                Console.WriteLine("\nReusing existing flyweight object.");
                return _drinkCache[drinkKey];
            }
            else
            {
                Console.WriteLine("\nCreating new flyweight object.");
                switch (drinkKey)
                {
                    case "Espresso":
                        drink = new Espresso();
                        break;
                    case "BananaSmoothie":
                        drink = new BananaSmoothie();
                        break;
                    default:
                        throw new Exception("This is not a flyweight drink object...");
                }
            }

            _drinkCache.Add(drinkKey, drink);
            ObjectsCreated++;

            return drink;
        }

        public IDrinkFlyweight CreateGiveaway()
        {
            return new DrinkGiveaway();
        }

        public void ListDrinks()
        {
            Console.WriteLine($"\nFactory has {_drinkCache.Count} drink objects ready to use.");
            Console.WriteLine($"Number of objects created: {ObjectsCreated}");

            foreach (var drink in _drinkCache)
                Console.WriteLine($"\t{drink.Value.Name}");

            Console.WriteLine("\n");
        }
    }
}

7.3 Program.cs

using System;

namespace Flyweight_Pattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var drinkFactory = new DrinkFactory();

            /*
            // Démonstration des flyweights partagés
            var largeEspresso = drinkFactory.GetDrink("Espresso");
            largeEspresso.Serve("Large");

            var mediumSmoothie = drinkFactory.GetDrink("BananaSmoothie");
            mediumSmoothie.Serve("Medium");

            var smallEspresso = drinkFactory.GetDrink("Espresso");
            smallEspresso.Serve("Small");

            drinkFactory.ListDrinks();
            */

            // Démonstration des flyweights non partagés (DrinkGiveaway)
            var sizes = new string[] { "Small", "Medium", "Large" };
            foreach (var size in sizes)
            {
                var giveaway = drinkFactory.CreateGiveaway();
                giveaway.Serve(size);
            }
        }
    }
}

7.4 Flyweight_Pattern.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

</Project>

Project configuration:

  • Type: Console application
  • Target framework: .NET Core 3.1
  • Namespace: Flyweight_Pattern

8. Use cases and applications

8.1 Pattern application criteria

Implementing all classes in a project as flyweights is not the right approach — it may even unnecessarily overload your codebase. Identify where the boss is truly helpful.

Scenarios adapted to the Flyweight pattern:

  1. Large quantity of objects: The application must manage a very large number of objects, which makes storage a problem.

  2. Mostly extrinsic state: Most object state can be converted to extrinsic state, and many different groups of objects can be generalized into a few shared objects.

  3. Identity independence: The application does not depend on the identity of the objects. Because flyweight objects are shared, identity tests (object.ReferenceEquals) will not be able to distinguish between shared references.

Concrete examples in the real world:

  • Video game engines (trees, enemies, particles)
  • Text editors (handling typographic characters)
  • Mapping systems (displaying millions of tiles)
  • Financial applications (financial instrument objects)

8.2 Impact on memory and performance

Memory savings:

  • The main implication of the Flyweight pattern is memory and storage savings
  • Savings are maximum when:
  • A large number of shared objects are in use
  • Stored intrinsic state AND calculated extrinsic state are both substantially used

Runtime costs:

  • There are runtime costs related to finding and calculating the extrinsic state of a shared object
  • If applied in the right situations, these costs are offset by the space savings from each additional shared flyweight

8.3 Integration with other patterns

The Flyweight pattern integrates well with other design patterns:

BossSynergy with Flyweight
CompositeGreat way to create logical hierarchies that must coexist with the Flyweight pattern. Hierarchical relationships can be maintained by passing references to parent objects or other objects in the hierarchy in the extrinsic state.
StateState pattern implementations work well for flyweight objects themselves
StrategyStrategy pattern implementations also work well for flyweight

9. Summary and conclusions

What we learned

At the end of this course, the following concepts have been covered:

  1. Create a flyweight interface — Define the IDrinkFlyweight contract with read-only properties for intrinsic state and methods for extrinsic state.

  2. Distinguish and implement intrinsic and extrinsic state — Understand that intrinsic state is immutable and shared, while extrinsic state is passed as a parameter on each call.

  3. Using a FlyweightFactory correctly — The factory is the only access point to the pattern; it manages a cache and avoids the creation of redundant objects.

  4. Understanding unshared flyweightsUnsharedConcreteFlyweight implements the interface but does not share their state; they make it possible to manage borderline cases while remaining within the framework of the pattern.

  5. Identify and work with the pattern in real-world scenarios — Recognize when the pattern is applicable and understand its implications on memory and performance.

Key Takeaways

  • Intrinsic state is private, immutable, and shared — never modify it from outside
  • Extrinsic state is passed as a parameter and must never be stored in a shared flyweight
  • The factory is the only access point — it manages the cache and decides whether to create or reuse
  • The pattern is not universal — use it only when the scenario really warrants it
  • UnsharedConcreteFlyweight are exception cases which allow more flexibility at the cost of no memory saving

Additional resources

  • C# Fundamentals course on Pluralsight (for beginners)
  • Courses on other C# design patterns in the Pluralsight library
  • Design Patterns: Elements of Reusable Object-Oriented Software — Gang of Four (original reference)
  • Course discussion section on Pluralsight (author monitors it regularly)

Search Terms

c-sharp · design · patterns · flyweight · testing · architecture · c# · .net · development · pattern · application · classes · concrete · extrinsic · factory · integration · state

Interested in this course?

Contact us to book it or get a custom training plan for your team.