CHAPTER 3 · DESIGN PATTERNS

Creational Design Patterns

Every program is full of the word new. It looks harmless, but every new ConcreteClass() hard-wires your code to one specific class — a decision you can never take back without editing that line. Creational patterns are five different ways to take the new out of your business logic, so a system becomes independent of how, which, who, and when its objects are created. Everything here is in C#; the lecture slides use Python, and we call out where the two languages genuinely differ.

Why creating objects is a design problem

When you write var truck = new Truck(); you have quietly made three commitments that are easy to miss:

That is tight coupling: your code now depends on a concrete type. The moment a second type shows up (a Ship, a Mac button, a crypto payment) you have to reopen and edit working code — which is exactly what the Open/Closed Principle tells you not to do, and it drags in dependencies the Dependency Inversion Principle says should point at abstractions, not concretions.

🧠 Why this matters

The GoF book states the whole family's job in one line: creational patterns "make a system independent of how its objects are created, composed, and represented." They do it two ways — a class creational pattern (Factory Method) uses inheritance to vary which class gets instantiated; an object creational pattern (Abstract Factory, Builder, Prototype) delegates instantiation to another object. Both share two goals: (1) encapsulate knowledge of which concrete classes the system uses, and (2) hide how those instances are created and wired together. The rest of the system only ever sees interfaces.

The five patterns are five answers to five different creation questions:

PatternOne-line intentThe question it answers
Factory MethodLet subclasses decide which class to instantiate."I create one product but can't know its class in advance."
Abstract FactoryCreate whole families of related products."My products must match each other and never mix."
BuilderConstruct a complex object step by step."My object has too many parts for one constructor."
PrototypeCreate new objects by cloning existing ones."Copying an existing object is cheaper than building fresh."
SingletonGuarantee exactly one instance, globally reachable."There must be one and only one of this thing."
💡 Tip

Read them in order. Factory Method is the foundation — Abstract Factory is "Factory Method, one level up", and both Abstract Factory and Prototype often collaborate. Singleton is last on purpose: it is the most abused pattern, and you'll appreciate the criticism more after seeing four disciplined patterns first.

Factory Method creational · class pattern

Intent: define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to its subclasses.

🌍 Real-world analogy

A logistics company knows the process of delivery — plan the route, load the goods, deliver. But how it delivers depends on the branch: the road branch sends a truck, the sea branch sends a ship. Head office writes the process once; each branch fills in "what vehicle." Head office never has to learn about ships — it just says "give me a transport" and the branch decides.

The problem: a logistics app welded to Truck

You build RoadRabbit, a logistics app. Version 1 only moves goods by truck, so the code is full of new Truck(). Then the company acquires a shipping firm and you must add ships. Here is where all that new comes back to bite you:

❌ Naive: the app decides the class with an if/else
public class LogisticsApp
{
    public void PlanDelivery(string transportType)
    {
        ITransport transport;

        // The pain point: every new vehicle forces an edit HERE.
        if (transportType == "truck")      transport = new Truck();
        else if (transportType == "ship")  transport = new Ship();
        else throw new ArgumentException("Unknown transport");

        transport.Deliver();   // the actual work
    }
}
Change request: "add air freight." You must reopen PlanDelivery, add another branch, and risk breaking truck and ship handling. The app knows too much about concrete classes. Violates OCP.
✅ With Factory Method
public abstract class Logistics
{
    // THE FACTORY METHOD — subclasses fill this in.
    public abstract ITransport CreateTransport();

    // Business logic that USES the product,
    // with zero knowledge of its concrete class.
    public void PlanDelivery()
    {
        ITransport transport = CreateTransport();
        transport.Deliver();
    }
}
Adding air freight = one new class (AirLogistics). PlanDelivery never changes. New behaviour by extension, not modification.

The solution: delegate the new to a subclass

The insight in one sentence: replace the direct new Truck() call with a call to an overridable factory method, and let each subclass answer "which product?" The object is still built with new — but that new now lives inside CreateTransport(), where it can be overridden. The things a factory method returns are called products.

Why an abstract class for Logistics rather than an interface? Since C# 8 an interface can carry a default method body, so PlanDelivery could technically live there — but an interface still can't hold instance state or fields, and shared stateful logic belongs in an abstract class. So the design choice stands: when there's common behaviour and state to share, reach for an abstract class; when there's only a contract, an interface. (In Python this whole thing was one ABC with an @abstractmethod.)

ITransport.cs — the product interface
// All products share ONE interface. The creator's factory method
// returns THIS type, so no client ever names a concrete class.
public interface ITransport
{
    void Deliver();
}
Transports.cs — concrete products
public class Truck : ITransport
{
    public void Deliver() => Console.WriteLine("Delivering by land in a box.");
}

public class Ship : ITransport
{
    public void Deliver() => Console.WriteLine("Delivering by sea in a container.");
}
Logistics.cs — the abstract creator
public abstract class Logistics
{
    // The factory method: declared, not implemented.
    // Return type is the INTERFACE, not a concrete class.
    public abstract ITransport CreateTransport();

    // Shared business logic. Notice it calls CreateTransport()
    // without knowing (or caring) what it returns.
    public void PlanDelivery()
    {
        ITransport transport = CreateTransport();
        transport.Deliver();
    }
}
ConcreteCreators.cs — subclasses pick the product
public class RoadLogistics : Logistics
{
    public override ITransport CreateTransport() => new Truck();
}

public class SeaLogistics : Logistics
{
    public override ITransport CreateTransport() => new Ship();
}
Program.cs — the client
static void Run(Logistics company)
{
    // The client treats every company the same way.
    // It never writes "new Truck" or checks a type.
    company.PlanDelivery();
}

Run(new RoadLogistics());   // Delivering by land in a box.
Run(new SeaLogistics());    // Delivering by sea in a container.
Logistics «abstract Creator» + PlanDelivery() + CreateTransport()* RoadLogistics CreateTransport() SeaLogistics CreateTransport() ITransport «Product» + Deliver() Truck Ship «creates»
Figure: The creator declares CreateTransport() (the * marks it abstract). Subclasses override it to return a concrete product. The creator's own PlanDelivery() depends only on the ITransport interface.
Try it: Factory Method — swap the branch, not the process
What to notice: the client always calls the same PlanDelivery(). When "AirLogistics" was added, no existing code changed — a new creator subclass just returns a new product. That is the Open/Closed Principle in action.
🔀 Easily confused: Factory Method vs a "Simple Factory"

Students mix these up constantly. A Simple Factory is just a static method (or a dictionary) that switches on a string and returns a product:

public static class TransportFactory
{
    public static ITransport Create(string kind) => kind switch
    {
        "truck" => new Truck(),
        "ship"  => new Ship(),
        _ => throw new ArgumentException(nameof(kind))
    };
}

It is handy, but it is not a GoF pattern — and notice the switch is the very if/else we set out to kill: add a type and you edit this method (still violates OCP). The real Factory Method uses inheritance and overriding — no central switch, each subclass supplies its own product. Use a Simple Factory for trivial cases; use Factory Method when the choice of product belongs to a subclass with its own logic. Chapter 12 (Confusable Patterns) lays Factory Method beside Abstract Factory in full.

When to use it (applicability, from the slides):

The cost: you may need a whole parallel hierarchy of creators just to introduce one product. If the choice is trivial, a Simple Factory or plain constructor is lighter. Don't build subclasses you don't need.

ℹ️ Where .NET uses it

WebRequest.Create(url), a registered Func<IService> delegate, and IHttpClientFactory.CreateClient() are all factory-style APIs in the same spirit — a single entry point that hides which concrete type comes back and picks it by a string or scheme instead of you calling new — but none is the GoF pattern above: there's no creator hierarchy with an overridable creation operation. For the real thing in the BCL, look at System.Data.Common.DbProviderFactory: each ADO.NET provider (SqlClientFactory, NpgsqlFactory, …) subclasses it and overrides CreateConnection()/CreateCommand() to return its own provider-specific types.

Abstract Factory creational · object pattern

Intent: provide an interface for creating families of related or dependent objects without specifying their concrete classes. Think of it as "Factory Method, one level up" — a factory that produces not one product but a matched set.

🌍 Real-world analogy

A furniture showroom sells styles: Modern and Victorian. If you order a Modern living-room set you get a Modern chair and a Modern sofa — never a Modern chair next to a Victorian sofa. The "style" is the factory; it guarantees everything it hands you matches. You pick the style once; consistency comes for free.

The problem: a cross-platform UI that keeps mixing families

You're building a UI toolkit that must run on Windows and macOS, with two widgets: Button and Checkbox. Without a pattern you check the OS every place you build a widget — and sooner or later two checks disagree:

❌ Naive: check the OS at every creation site
string os = "Windows";

// In the login screen…
IButton btn = os == "Windows" ? new WinButton() : new MacButton();
ICheckbox chk = os == "Windows" ? new WinCheckbox() : new MacCheckbox();

// …later, in a totally different file (settings page):
IButton settingsBtn = os == "Windows" ? new WinButton() : new MacButton();
// Oops — a developer forgets the check here:
ICheckbox settingsChk = new MacCheckbox();   // Windows button + Mac checkbox = Frankenstein UI
Two problems: the os == "Windows" check is repeated everywhere (violates DRY), and nothing stops you mixing families. One missed check and your Windows app grows a macOS checkbox.

The solution: one factory object that makes the whole family

The insight in one sentence: put all the "create X" methods behind a single factory interface, then write one concrete factory per family — so choosing a family is a single decision made once, and mixing becomes impossible by construction. The client receives a factory and asks it for widgets; it never sees a concrete class and never checks the OS again.

Widgets.cs — the abstract products
// Each PRODUCT in the family has its own interface.
public interface IButton   { void Render(); }
public interface ICheckbox { void Render(); }
ConcreteWidgets.cs — one variant per family
// --- Windows family ---
public class WinButton   : IButton   { public void Render() => Console.WriteLine("[Win] square button"); }
public class WinCheckbox : ICheckbox { public void Render() => Console.WriteLine("[Win] checkbox"); }

// --- macOS family ---
public class MacButton   : IButton   { public void Render() => Console.WriteLine("[Mac] rounded button"); }
public class MacCheckbox : ICheckbox { public void Render() => Console.WriteLine("[Mac] checkbox"); }
IGuiFactory.cs — the abstract factory
// One creation method per product in the family.
// A factory MUST supply the whole set — that's the contract.
public interface IGuiFactory
{
    IButton   CreateButton();
    ICheckbox CreateCheckbox();
}
ConcreteFactories.cs — one factory per family
public class WindowsFactory : IGuiFactory
{
    public IButton   CreateButton()   => new WinButton();
    public ICheckbox CreateCheckbox() => new WinCheckbox();
}

public class MacFactory : IGuiFactory
{
    public IButton   CreateButton()   => new MacButton();
    public ICheckbox CreateCheckbox() => new MacCheckbox();
}
Application.cs — the client
public class Application
{
    private readonly IButton _button;
    private readonly ICheckbox _checkbox;

    // The app gets a factory and NEVER checks the OS again.
    public Application(IGuiFactory factory)
    {
        _button   = factory.CreateButton();
        _checkbox = factory.CreateCheckbox();   // guaranteed same family
    }

    public void Render() { _button.Render(); _checkbox.Render(); }
}

// The ONE place the OS is checked — at startup:
IGuiFactory factory = os == "Windows" ? new WindowsFactory() : new MacFactory();
new Application(factory).Render();
IGuiFactory «Abstract Factory» CreateButton() / CreateCheckbox() WindowsFactory MacFactory WinButton WinCheckbox MacButton MacCheckbox Windows family (always matches) macOS family (always matches)
Figure: Each concrete factory owns one vertical "family" column. Because a factory only builds its own column, the client can never end up with a cross-family mix.
Try it: Abstract Factory — pick a family, get a matched set
What to notice: "Build with factory" always logs two widgets from the same family. "Build by hand" simulates scattered new calls where a developer forgets the OS check — watch it occasionally emit a mismatched pair. The factory makes that bug structurally impossible.
⚠️ Watch out — adding a new product is expensive

Abstract Factory is great at adding a new family (write one LinuxFactory, done). But adding a new product to every family — say a Slider — means editing the factory interface and every concrete factory. That's the pattern's built-in trade-off: families are cheap to add, product types are not. The slides note a concrete factory is often a Singleton, and is itself usually implemented with Factory Methods.

When to use it (applicability, from the slides):

ℹ️ Where .NET uses it

DbProviderFactory is the classic example: SqlClientFactory, MySqlClientFactory, etc. each produce a matched family of DbConnection, DbCommand, and DbParameter — you never mix a SQL Server connection with a MySQL command. Cross-platform UI frameworks and theming engines use the same shape to keep a whole widget set on-brand.

🔀 Easily confused: Abstract Factory vs Factory Method

Both hide new behind an abstraction, so they blur together. The clean distinction: Factory Method makes one product via inheritance (override a method); Abstract Factory makes a family of products via composition (you hold a factory object and call several methods on it). Rule of thumb: one product → Factory Method; a set that must stay consistent → Abstract Factory. See the full side-by-side in Chapter 12.

Builder creational · object pattern

Intent: separate the construction of a complex object from its representation, so the same construction process can create different representations. In plain terms: build a big object step by step, calling only the steps you need.

🌍 Real-world analogy

Ordering at Subway. You don't hand the worker a 10-argument order form ("white, turkey, no, cheddar, yes, mayo, no, toasted, …"). You walk the line: pick bread, add meat, add cheese, choose veggies, add sauce — skipping any step you don't want. Same process, endless different sandwiches. The Director is the worker who knows the standard recipes ("the Club," "the Veggie"); the Builder is the set of stations.

The problem: the telescoping constructor

You run a custom-PC shop. A Computer has a CPU, RAM, storage, GPU, cooling, case, power supply, RGB… Trying to cover every configuration in one constructor gives you the telescoping constructor anti-pattern:

❌ Naive: one monstrous constructor
public class Computer
{
    public Computer(string cpu, string ram, string storage, string gpu,
                    string cooling, string @case, string psu,
                    bool rgb, bool wifi) { /* assign 9 fields */ }
}

// The call site is unreadable. What is 'true, false' here?
var pc = new Computer("i9", "32GB", "1TB SSD", "RTX 4090",
                      "Air", "Tower", "850W", true, false);
You can't tell what true, false mean without opening the constructor. An office PC with no GPU still has to pass null. And the "fix" of subclassing every combination explodes: the slides note that 5 optional parameters ⇒ up to 120 constructor overloads. Rigid and unreadable.
🎓 Lecture view vs reality

For the exam: the slides say 5 optional parameters give up to 120 constructor overloads. In practice: 120 = 5! counts orderings, not configurations. Five independent optional parts give 2⁵ = 32 possible configurations; the telescoping-constructor workaround only needs up to n+1 increasingly long overloads (6 here). Either way the point holds — the count explodes and one constructor can't sanely cover it.

The solution: extract construction into a Builder (+ optional Director)

The insight in one sentence: move the construction code out of the product into a separate builder object that exposes one method per step, and let the caller run only the steps it needs. The four roles:

  1. Product — the complex object (Computer). It loses its giant constructor.
  2. Builder — declares the construction steps (SetCpu, SetGpu…).
  3. Concrete Builder — implements those steps and holds the part being assembled (GamingPcBuilder).
  4. Director (optional) — knows recipes: fixed sequences of steps you want to reuse.
Computer.cs — the product
// No giant constructor. Parts are added one at a time.
public class Computer
{
    private readonly List<string> _parts = new();
    public void Add(string part) => _parts.Add(part);
    public override string ToString() => string.Join(", ", _parts);
}
IComputerBuilder.cs — the step interface
// Returning the builder itself (IComputerBuilder) enables
// fluent method chaining: builder.SetCpu(..).SetRam(..).
public interface IComputerBuilder
{
    IComputerBuilder Reset();
    IComputerBuilder SetCpu(string cpu);
    IComputerBuilder SetRam(string ram);
    IComputerBuilder SetGpu(string gpu);
    Computer GetResult();
}
GamingPcBuilder.cs — a concrete builder
public class GamingPcBuilder : IComputerBuilder
{
    private Computer _pc = new();

    public IComputerBuilder Reset()            { _pc = new Computer(); return this; }
    public IComputerBuilder SetCpu(string cpu) { _pc.Add($"CPU: {cpu}");  return this; }
    public IComputerBuilder SetRam(string ram) { _pc.Add($"RAM: {ram}");  return this; }
    public IComputerBuilder SetGpu(string gpu) { _pc.Add($"GPU: {gpu}");  return this; }

    public Computer GetResult()
    {
        Computer built = _pc;
        Reset();            // ready for the next build
        return built;
    }
}
Director.cs — reusable recipes
// The Director knows the ORDER of steps for common builds.
// It is optional — the client can call steps directly.
public class Director
{
    public Computer BuildGamingRig(IComputerBuilder b) =>
        b.Reset().SetCpu("Intel i9").SetRam("32GB DDR5").SetGpu("RTX 4090").GetResult();

    public Computer BuildOfficePc(IComputerBuilder b) =>
        b.Reset().SetCpu("Intel i5").SetRam("16GB").GetResult();  // no GPU step
}
Program.cs — the client
var director = new Director();
var builder  = new GamingPcBuilder();

Computer rig    = director.BuildGamingRig(builder);
Computer office = director.BuildOfficePc(builder);

// Or skip the Director and build a custom machine directly:
Computer custom = builder.Reset().SetCpu("Ryzen 7").SetGpu("RTX 4070").GetResult();

Console.WriteLine(rig);      // CPU: Intel i9, RAM: 32GB DDR5, GPU: RTX 4090
Console.WriteLine(office);   // CPU: Intel i5, RAM: 16GB
Director BuildGamingRig() IComputerBuilder SetCpu() SetRam() SetGpu() GetResult() GamingPcBuilder Computer «Product» uses builds
Figure: The Director drives the Builder interface; the concrete builder implements the steps and yields the finished Product. Same steps + different builders = different products.
Try it: Builder — assemble a PC one step at a time
What to notice: you call only the steps you want — no null arguments. The two Director buttons run fixed recipes on the same builder, and GetResult() hands back the finished product and resets for the next build.
Deep dive: Builder vs C#'s object initializers and with records

C# has two language features that solve part of what Builder solves, and you should know when they're enough:

Object initializers — for "just set some properties"
public class Computer
{
    public string Cpu { get; init; } = "";
    public string Ram { get; init; } = "";
    public string? Gpu { get; init; }           // optional
}

// Named, order-free, optional — no telescoping constructor needed:
var pc = new Computer { Cpu = "i9", Ram = "32GB", Gpu = "RTX 4090" };

This kills the "unreadable positional arguments" problem for free. Reach for the full Builder pattern when construction involves real work — validation, ordering constraints, building nested parts, or producing different representations from the same steps (e.g. a builder that outputs a Computer vs one that outputs an XML spec). Initializers just assign fields; they can't run a multi-step algorithm.

Records + with — for "copy this but change one thing"
public record NetworkRequest(string Url, string? Auth = null, int Cache = 0);

var baseReq = new NetworkRequest("google.com");
var authed  = baseReq with { Auth = "abc123", Cache = 60000 };  // non-destructive copy

The with expression is a language-level cousin of both Builder (incremental configuration) and Prototype (copy-then-tweak). The lecture's Python notebook shows this exact NetworkService builder; in C# a record often replaces it outright.

When to use it (applicability, from the slides):

The cost: more moving parts (a builder class per representation, maybe a director). Overkill for objects with two or three fields — use a constructor or an object initializer there.

ℹ️ Where .NET uses it

StringBuilder (append steps, then ToString()), ASP.NET Core's WebApplicationBuilder / HostBuilder (configure services step by step, then Build()), UriBuilder, and EF Core's fluent ModelBuilder are all this pattern. The tell-tale sign is a chain of configuring calls ending in .Build() or .ToString().

🔀 Easily confused: Builder vs Abstract Factory

Both build complex objects, so the slides flag them explicitly. The difference: Builder constructs one product step by step and returns it as a final step (GetResult()); Abstract Factory emphasises families of products and returns each one immediately. Builder cares about the how and order of assembly; Abstract Factory cares about which matched set. (A Composite is often what a Builder builds.) Full comparison in Chapter 12.

Prototype creational · object pattern · aka Clone

Intent: specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype. In short: build a new object by cloning an existing one, without your code knowing its concrete class.

🌍 Real-world analogy

The slides pick a lovely one: mitotic cell division. An industrial prototype doesn't copy itself, but a living cell does — it divides into two identical cells. The original cell is the prototype and takes an active role in creating its copy. That's the pattern: the object clones itself, because only it knows all of its own fields (including the private ones).

The problem: copying an object from the outside

Say you want an exact copy of an existing configured object. Doing it "by hand" from outside is surprisingly broken:

❌ Naive: copy field-by-field from outside
// You want a duplicate of 'original'…
var copy = new Character();
copy.Name  = original.Name;
copy.Level = original.Level;
copy.Home  = original.Home;      // and you must KNOW every field
// copy.Secret = original.Secret; // …but private fields aren't visible from here!
Three problems: you must know the concrete class (coupling), you must know every field (and keep the copy in sync forever), and you can't reach private fields from outside the object. Change request: "add a field" — every copy site is now silently wrong.

The solution: let the object clone itself

The insight in one sentence: declare a common Clone() method and let each class copy itself — it can see its own private fields, and callers never name the concrete type. In C# the base object.MemberwiseClone() does the field-copy for you. But that is exactly where the famous trap lives:

⚠️ Watch out — shallow vs deep copy (the #1 Prototype bug)

MemberwiseClone() makes a shallow copy: value-type fields (int, bool, struct) are copied by value, but reference-type fields (objects) are copied by reference — the clone and the original end up pointing at the same nested object. Mutate the clone's nested field and you've mutated the original's too. A deep copy also clones the nested objects, so the two are fully independent.

🩹 Shallow clone — shares nested objects
public class Address { public string City = ""; }

public class Character
{
    public string Name = "";
    public Address Home = new();

    // Shallow: Home reference is COPIED, not the Address itself.
    public Character ShallowClone() => (Character)MemberwiseClone();
}

var c2 = c1.ShallowClone();
c2.Home.City = "Kandy";   // ⚠️ also changes c1.Home.City —
                          // both point at the SAME Address!
Fine when nested fields are immutable; a bug waiting to happen when they're mutable.
✅ Deep clone — copies nested objects too
public class Character
{
    public string Name = "";
    public Address Home = new();

    public Character DeepClone()
    {
        var copy = (Character)MemberwiseClone();   // top-level fields
        copy.Home = new Address { City = Home.City }; // clone the nested one
        return copy;
    }
}

var c2 = c1.DeepClone();
c2.Home.City = "Kandy";   // ✅ c1.Home.City is untouched —
                          // c2 has its OWN Address.
Fully independent copy. Do this whenever nested objects are mutable.
ℹ️ Note — the Prototype Registry

The slides describe a companion piece: a Prototype Registry, a store of pre-built, ready-to-copy prototypes. The simplest one is a Dictionary<string, IPrototype> (name → prototype); ask it for "goblin" and it hands back a fresh clone. It centralises your "template" objects so callers clone by name instead of by class.

public class ShapeRegistry
{
    private readonly Dictionary<string, Shape> _prototypes = new();
    public void Register(string key, Shape p) => _prototypes[key] = p;
    public Shape Get(string key) => _prototypes[key].Clone();   // returns a COPY
}
Try it: Prototype — clone, then mutate the clone's nested field
What to notice: mutate the clone's nested city. In shallow mode the original's city changes too (they share one Address). In deep mode the original is safe. Same button, opposite outcome — that's the whole shallow/deep story.

When to use it: when your classes to instantiate are specified at runtime; when creating an object is costly (e.g. it required a big DB query) and copying a ready one is cheaper; or when objects have dozens of fields and hundreds of configurations — cloning a configured prototype beats subclassing for every combination. The cost: cloning objects with circular references or deep graphs is fiddly, and every class must implement its own correct deep clone.

ℹ️ Where .NET uses it

The with expression on records (var b = a with { X = 1 };) is Prototype built into the language — non-destructive copy of an existing instance. Array.Clone() and ICloneable exist too, but avoid ICloneable: its single Clone() never says whether it's shallow or deep, which is precisely the ambiguity that causes bugs. Prefer an explicit Clone()/DeepClone() of your own, or records with with.

🔀 Easily confused: Prototype vs Abstract Factory

The slides call them "competing patterns" — both give you new objects without naming concrete classes. The difference in mechanism: Abstract Factory constructs fresh objects via factory methods; Prototype copies an existing configured instance. They also combine — an Abstract Factory can store a set of prototypes and clone them to produce products. More in Chapter 12.

Singleton creational · object pattern

Intent: ensure a class has exactly one instance, and provide a global point of access to it. Common uses from the slides: a network manager, database access, logging, utility classes.

🌍 Real-world analogy

A country has one government. Whoever the officials are, "the government of X" always refers to the same single body, and there's a well-known way to reach it. You don't get to spin up a second parallel government — one instance, globally accessible.

The solution: private constructor + static accessor

The insight in one sentence: make the constructor private so nobody outside can call new, and expose a single static accessor that creates the instance once and returns the cached one thereafter. Two mandatory steps, every time:

  1. Make the default constructor private — blocks new Logger() from anywhere else.
  2. Add a static creation method/property that lazily creates and caches the sole instance.
❌ Naive lazy init — NOT thread-safe
public sealed class Logger
{
    private static Logger? _instance;
    private Logger() { }   // step 1: block external 'new'

    public static Logger Instance
    {
        get
        {
            // step 2 — but this check-then-create is a RACE:
            if (_instance == null)      // two threads can BOTH see null…
                _instance = new Logger(); // …and BOTH create an instance.
            return _instance;
        }
    }
}
On a multi-threaded server two threads can pass the null check simultaneously and create two instances — breaking the one-and-only guarantee.
✅ The idiomatic C# fix: Lazy<T>
public sealed class Logger
{
    // Lazy<T> is thread-safe by default and creates
    // the value only on first access.
    private static readonly Lazy<Logger> _lazy =
        new(() => new Logger());

    private Logger() { }

    public static Logger Instance => _lazy.Value;
}
Correct, lazy, and thread-safe with no manual locking. This is the version to reach for in C#.
Deep dive: the thread-safety ladder — lock, double-check, eager, and Lazy<T>

1. Lock every access — correct but slow; every caller pays for a lock even after the instance exists:

private static Logger? _instance;
private static readonly object _gate = new();
public static Logger Instance
{
    get { lock (_gate) { return _instance ??= new Logger(); } }
}

2. Double-checked locking — lock only on the first race; the outer check skips the lock once built:

private static volatile Logger? _instance;   // 'volatile' matters here
public static Logger Instance
{
    get
    {
        if (_instance == null)               // fast path, no lock
            lock (_gate)
                if (_instance == null)        // re-check inside the lock
                    _instance = new Logger();
        return _instance;
    }
}

3. Eager (static field) — the CLR guarantees a static initializer runs once; simplest thread-safe form, but builds the instance even if you never use it:

public sealed class Logger
{
    public static readonly Logger Instance = new();
    private Logger() { }
}

4. Lazy<T> (shown above) — gives you lazy and thread-safe with none of the boilerplate. In modern C# this is the default choice; the manual versions are worth understanding so you know what Lazy<T> is doing for you. The lecture's Python notebook demonstrates the same race with a metaclass and then fixes it with a Lock — identical idea, different syntax.

Try it: Singleton — watch the race create two instances
What to notice: in naive mode the two "threads" interleave — both see _instance == null before either assigns, so you get two different instance ids (bug!). In locked mode the second thread waits, sees the instance already exists, and returns the same id.
⚠️ Watch out — Singleton is the most-abused pattern. Here's why.

The slides admit it up front: Singleton "solves two problems at the same time, violating the Single Responsibility Principle" — it controls its own lifetime and acts as a global access point. The deeper costs:

  • Global mutable state. Any code can reach it and change it, so behaviour depends on hidden order-of-access — the exact thing OO tried to escape.
  • Hard to test. A static Instance can't be swapped for a fake/mock, so unit tests that touch it become coupled and non-isolated (state leaks between tests).
  • Hidden dependencies. A class that calls Logger.Instance internally doesn't advertise that dependency in its constructor — you can't tell what it needs by reading its signature.

The modern alternative: dependency injection. Register the type once with a singleton lifetime in a DI container and inject it through the constructor. You still get one shared instance, but now the dependency is explicit, mockable in tests, and not a global:

// ASP.NET Core: one instance for the whole app, but injected, not global.
services.AddSingleton<ILogger, Logger>();

public class OrderService
{
    private readonly ILogger _log;
    public OrderService(ILogger log) => _log = log;  // dependency is VISIBLE and swappable
}

When to use it (applicability, from the slides): when there must be exactly one instance available to all clients (e.g. one shared database object), and when you need stricter control than a global variable — unlike a global, the Singleton guarantees nothing can replace the cached instance. Even so, prefer DI's singleton lifetime for anything testable.

ℹ️ Where .NET uses it

Effective singletons are everywhere as cached static instances: Comparer<T>.Default, Encoding.UTF8, Task.CompletedTask. But the framework's recommended way to share one instance is not the classic pattern — it's the DI container's AddSingleton lifetime, for exactly the testability reasons above.

🔀 Easily confused: Singleton vs a static class vs global variable

A static class can't implement an interface or be passed around, so it can't be mocked or substituted — a Singleton is a real object and can. A global variable can be reassigned; a Singleton guarantees the one instance can't be replaced. And a concrete factory in Abstract Factory is often a Singleton. See Chapter 12 for the full disambiguation.

Choosing: which creational pattern?

All five take the new out of your business logic — the question is which creation problem you have. Answer the questions in the tree, or use the interactive chooser below.

Need to create objects flexibly Exactly ONE instance, globally shared? Singleton yes A whole FAMILY of matched products? Abstract Factory yes Complex object, built step by step? Builder yes Cheaper to COPY an existing configured object? Prototype yes ONE product, but a subclass should decide which? Factory Method yes
Figure: A rough decision tree. These aren't mutually exclusive — an Abstract Factory is often built from Factory Methods, and a concrete factory is often a Singleton.
Try it: "Which creational pattern?" chooser
Do you need to guarantee exactly one shared instance?
What to notice: each answer narrows the intent. The chooser walks the same tree above and lands on one pattern with a one-line reason.

The five, side by side

PatternCreatesMechanismVary atKey benefitMain cost
Factory Method One product Inheritance — override a method Compile time (subclass) New product = new subclass; client untouched A parallel creator hierarchy
Abstract Factory A family of products Composition — hold a factory object Runtime (pick a factory) Guarantees products match; no mixing Adding a new product type touches every factory
Builder One complex product, step by step Composition — a builder + optional director Runtime (which steps) No telescoping constructor; many representations Extra builder/director classes
Prototype A copy of an existing object Cloning — object copies itself Runtime (which prototype) Cheap copies; no coupling to class Deep-clone correctness; object graphs
Singleton One instance, reused Private ctor + static accessor n/a (there's only one) One guaranteed, globally reachable instance Global state; hard to test — prefer DI

Check yourself — chapter quiz