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:
- Which concrete class exists (
Truck) — not an interface, the real class. - How it is built — which constructor, which arguments, in which order.
- Where that knowledge lives — right here, in the middle of your business logic.
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.
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:
| Pattern | One-line intent | The question it answers |
|---|---|---|
| Factory Method | Let subclasses decide which class to instantiate. | "I create one product but can't know its class in advance." |
| Abstract Factory | Create whole families of related products. | "My products must match each other and never mix." |
| Builder | Construct a complex object step by step. | "My object has too many parts for one constructor." |
| Prototype | Create new objects by cloning existing ones. | "Copying an existing object is cheaper than building fresh." |
| Singleton | Guarantee exactly one instance, globally reachable. | "There must be one and only one of this thing." |
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.
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:
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
}
}
PlanDelivery, add another
branch, and risk breaking truck and ship handling. The app knows too much about concrete classes. Violates OCP.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();
}
}
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.)
// 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();
}
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.");
}
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();
}
}
public class RoadLogistics : Logistics
{
public override ITransport CreateTransport() => new Truck();
}
public class SeaLogistics : Logistics
{
public override ITransport CreateTransport() => new Ship();
}
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.
CreateTransport() (the * marks it abstract). Subclasses
override it to return a concrete product. The creator's own PlanDelivery() depends only on the ITransport interface.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.
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):
- A class can't anticipate the class of objects it must create.
- A class wants its subclasses to specify the objects it creates.
- You want to localise the knowledge of which helper subclass is the delegate.
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.
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.
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:
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
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.
// Each PRODUCT in the family has its own interface.
public interface IButton { void Render(); }
public interface ICheckbox { void Render(); }
// --- 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"); }
// 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();
}
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();
}
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();
new calls where a developer forgets the OS check — watch it occasionally
emit a mismatched pair. The factory makes that bug structurally impossible.
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):
- A system should be independent of how its products are created, composed and represented.
- A system must be configured with one of several families of products.
- A family of related products is designed to be used together and you must enforce that.
- You ship a library and want to reveal only interfaces, not implementations.
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.
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.
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:
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);
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.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:
- Product — the complex object (
Computer). It loses its giant constructor. - Builder — declares the construction steps (
SetCpu,SetGpu…). - Concrete Builder — implements those steps and holds the part being assembled (
GamingPcBuilder). - Director (optional) — knows recipes: fixed sequences of steps you want to reuse.
// 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);
}
// 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();
}
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;
}
}
// 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
}
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
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:
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.
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 algorithm for creating a complex object should be independent of the parts and how they're assembled.
- The construction process must allow different representations of the constructed object.
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.
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().
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.
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:
// 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!
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:
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.
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!
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.
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
}
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.
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.
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.
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:
- Make the default constructor
private— blocksnew Logger()from anywhere else. - Add a static creation method/property that lazily creates and caches the sole instance.
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;
}
}
}
null check simultaneously and
create two instances — breaking the one-and-only guarantee.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;
}
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.
_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.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
Instancecan'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.Instanceinternally 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.
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.
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.
The five, side by side
| Pattern | Creates | Mechanism | Vary at | Key benefit | Main 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 |