When I started, design patterns felt like twenty-three names to memorise in three groups. In daily work I just wrote whatever made sense, like a Dictionary<string, Func<IExporter>>, until a reviewer told me I had built a factory without knowing it.
The list was never the hard part. The hard part is knowing which pattern to use, and that depends on what keeps changing in your system, not on what the pattern is called. So keep the list aside and walk along the production line instead.
A car has three stories inside it. First, how it gets built: which plant makes which part, in what order the stations work, and which parts a car can have only one of. Second, how the parts fit together: the dongle that lets a wireless phone talk to a USB-only music system, and the four safety modules between your foot and the brake. Third, what happens once someone drives it out of the showroom: the key fob that wakes four systems at once, and the gear lever that changes what every other control does.
These are the three families. Creational patterns build the car, structural patterns fit it together, and behavioural patterns decide how it behaves on the road. Every pin below is one pattern, coloured by its family. Click one.
Nine patterns, one vehicle
Pins 1–3 are about how the car is built. Pins 4–6 are about how its parts connect. Pins 7–9 are about how it behaves once someone drives it.
01 — CREATIONALHow the car gets built
Creational patterns all deal with one moment: new. Every new Thing() locks in a decision at that line. The caller has named one exact class, and is now tied to its constructor, its lifetime and that class alone. You do not notice this until the day you need a second version. A car has that moment a few thousand times.
01Factory Method — the seat plant
Nobody on the assembly line phones a supplier when a car body needs a seat. The station asks for "a seat for this body", and the seat plant decides: a cloth bench for the base model, heated leather for the top model, a fixed bucket seat for the track edition. The line takes the seat and bolts it in.
The base class decides when a seat is made and what happens to it. A subclass decides which seat arrives.
public abstract class TrimLine
{
protected abstract ISeat MakeSeat(); // the factory method
public Cabin Fit() // the part that never changes
{
var seat = MakeSeat();
return new Cabin(seat, Harness.Standard);
}
}
public sealed class BaseTrim : TrimLine
{
protected override ISeat MakeSeat() => new ClothBench(rows: 2);
}
public sealed class TrackTrim : TrimLine
{
protected override ISeat MakeSeat() => new BucketSeat(harnessPoints: 6);
}
You see this shape all the time in .NET, without the formal name. IHttpClientFactory is one. So is any DI registration where the caller asks for IPaymentGateway and gets StripeGateway. Use it when there is a family of things, more than one version, and a rule for choosing that the caller should not need to know.
02Builder — same stations, different vehicle
The line runs the same five stations for every car: platform, engine, body, interior and electronics. The order never changes; only what goes in at each station changes. A 1.2-litre petrol engine and cloth seats on this car, a diesel and an empty cargo area on the next.
You need a builder when a constructor has too many parameters, half of them true/false flags. new Vehicle(true, false, 5, null, "steel", true) is a puzzle every time you read it, and adding a seventh option means changing every caller. A builder gives each value a name, and gives you one place, Build(), to check everything before a broken object is ever created.
var hatchback = new VehicleBuilder()
.Platform("B-segment")
.Powertrain(Engine.Petrol12, Gearbox.Manual6) // each station returns this
.Body(doors: 5, shell: "steel")
.Interior(Trim.Cloth, seats: 5)
.Electronics(Cluster.Base)
.Build(); // throws if the powertrain is missing
If you have written an ASP.NET Core Program.cs, you have already used one. WebApplication.CreateBuilder(args) holds a half-set-up application until Build() turns it into something you can run.
03Singleton — one ECU
A car has exactly one engine control unit (ECU) deciding fuel and ignition timing. A second board would be dangerous, not a backup, because now two units disagree about how much fuel goes to the engine.
public sealed class EngineControlUnit
{
private static readonly Lazy<EngineControlUnit> _ecu
= new(() => new EngineControlUnit());
public static EngineControlUnit Instance => _ecu.Value; // thread-safe by construction
private EngineControlUnit() { }
}
Having only one instance is not the problem. The problem is EngineControlUnit.Instance showing up in the middle of a method. That is a hidden dependency: a test cannot replace it, and the class signature does not even mention it. Modern .NET gives you the same "only one" promise without the hard-coding.
builder.Services.AddSingleton<IEngineControlUnit, EngineControlUnit>();
// the consumer declares what it needs — and a test can hand it a bench rig
public sealed class FuelTrim(IEngineControlUnit ecu) { }
The same promise, now visible in the constructor and easy to replace in a test. Use the static version only when you have no DI container at all.
Creational patterns are about the moment of new — and who is allowed on the factory floor when it happens.
02 — STRUCTURALHow the parts fit together
Once the car body is on its wheels, the problems change. Nothing needs to be built now; things need to be connected. A wireless phone to a music system that only knows cables. Four safety modules between a pedal and a brake. Six systems behind one button. Each part works on its own, and now the whole thing must offer a simple way for someone to use it.
04Adapter — the dongle in the USB port
Your phone stopped using cables two upgrades ago. It pairs over Bluetooth and runs CarPlay over its own Wi-Fi. The car does not know any of this. Its music system was built in 2019 and understands only one thing: a phone at the end of a USB cable. You are not changing the car, and you are not changing the phone. So you buy a dongle the size of a matchbox, plug it into the USB port, and it pretends to be the cable.
You need an adapter when you control one side of the conversation but not the other. You cannot change the vendor's SDK, the partner's old SOAP service, or the internal service that three teams depend on. So you write the piece in the middle: wireless on one side, wired on the other, and both sides keep believing they got what they asked for.
public interface IWiredPhone // all the head unit knows how to talk to
{
Task<PhoneSession> ConnectAsync(UsbPort port);
}
public sealed class WirelessPhone // the phone in your pocket — not yours to change
{
public Task<WifiLink> PairAsync(BluetoothId id) => WifiLink.NegotiateAsync(id);
}
public sealed class WirelessDongle : IWiredPhone
{
private readonly WirelessPhone _phone;
public WirelessDongle(WirelessPhone phone) => _phone = phone;
public async Task<PhoneSession> ConnectAsync(UsbPort port)
{
var link = await _phone.PairAsync(port.LastPairedDevice); // wireless on one side
return PhoneSession.OverUsb(port, link.Stream); // wired on the other
}
}
In enterprise work, this is the pattern you will write most often, so decide carefully where adapters live: at the edge of the solution, in one project, named after what they wrap. The moment an adapter starts taking business decisions instead of just converting formats, it has become a service you forgot to name.
05Decorator — the brake stack
A brake is simple: your foot pushes fluid, the fluid squeezes the brake calliper. Then ABS sits in between and releases pressure fifteen times a second. Then EBD balances braking between the front and back wheels. Then traction control brakes a wheel you never asked it to. Then emergency braking presses the pedal for you. Each module takes the same command, adds its own behaviour and passes it on. The calliper never knows any of them exist.
One rule makes this work: the wrapper implements the same interface as the thing it wraps, and keeps a reference to it. Because of this, you can stack modules in any order, at runtime, without writing a class for every combination. Four options would otherwise need sixteen classes.
public interface IBrake { BrakeResult Apply(PedalInput input); }
public abstract class BrakeModule : IBrake
{
protected readonly IBrake Inner;
protected BrakeModule(IBrake inner) => Inner = inner;
public virtual BrakeResult Apply(PedalInput input) => Inner.Apply(input);
}
public sealed class Abs : BrakeModule
{
public Abs(IBrake inner) : base(inner) { }
public override BrakeResult Apply(PedalInput input)
=> input.WheelsLocking ? Inner.Apply(input.Pulsed()) : Inner.Apply(input);
}
IBrake pedal = new Aeb(new TractionControl(new Abs(new Calliper())));
You already use decorators every day in .NET under other names: ASP.NET Core middleware around a request, a DelegatingHandler around an HTTP handler, a Polly retry policy wrapped around a timeout. Each one adds something on the way in, on the way out, or both, and the innermost call knows nothing about it.
06Facade — the start button
Pressing START is not one action. The car wakes up the body electronics, checks the key with the immobiliser, starts the fuel pump, cranks the starter, waits for the alternator, and then runs the dashboard self-test. Six systems, in an order that matters. Crank the engine before the immobiliser clears, and all you get is a very expensive click.
public sealed class StartButton
{
// the six subsystems, injected — each still usable on its own
public async Task<StartResult> PressAsync(KeyFob fob)
{
_body.WakeBus();
if (!_immobiliser.Accepts(fob)) return StartResult.Rejected;
await _fuel.PrimeAsync();
_starter.Crank();
await _alternator.WaitForFieldAsync(); // up before the load returns
return _cluster.SelfTest();
}
}
A facade does not hide or block anything. A mechanic can still control each module from a diagnostic laptop. It just gives the common path a name, so the order of steps is not copied into every caller. You know you need one when you see the same steps repeated in the same order: the same four calls in three controllers, and the third one is missing a step. That missing step is the bug the facade would have prevented.
Adapter changes the shape. Decorator adds behaviour. Facade makes the long sentence short. All three leave the original part exactly as it was.
03 — BEHAVIOURALHow it acts once it leaves the gate
The last family is about what happens between objects while the program runs: who tells whom, who decides, and what changes when the situation changes. A car on a transport truck has no behaviour. Give someone the key, and suddenly a button twenty metres away must reach four systems, and one lever must change what every other control means.
07Observer — the key fob
Someone presses unlock from the other side of a car park. Four things happen, and the key fob knows about none of them: the doors unlock, the mirrors fold out, the ground lights come on, and the driver's seat moves to a saved position. Add a fifth feature in next year's model, and the button stays the same.
C# has this pattern built into the language (events), which is why most .NET developers use it for years before learning its name.
public sealed class KeyFob
{
public event EventHandler<UnlockEventArgs>? Unlocked;
public void PressUnlock(DriverProfile profile)
=> Unlocked?.Invoke(this, new UnlockEventArgs(profile, DateTime.UtcNow));
}
fob.Unlocked += (_, e) => _doors.Unlatch();
fob.Unlocked += (_, e) => _mirrors.Unfold();
fob.Unlocked += (_, e) => _lights.Welcome(seconds: 30);
fob.Unlocked += async (_, e) => await _seats.RecallAsync(e.Profile);
Two things can hurt you. A subscription that is never removed keeps the subscriber alive as long as the publisher lives. In a long-running service, that is a memory leak you first notice in a production memory graph, so always unsubscribe, or use IObservable<T> and dispose it. Also, that async handler is fire-and-forget: if it throws an exception, nobody hears about it. Once listeners do real work, move them to a queue.
08Strategy — the drive modes
The accelerator has one job: ask for power. What you actually get depends on the drive mode. Eco softens the first half of the pedal and changes gear early. Sport makes it sharp and holds each gear longer. Snow softens the first third, so the wheels do not spin when you leave the driveway. The pedal does not care; every mode takes the same request.
Strategy is how you get rid of long switch blocks over an enum. Each branch becomes a class, the enum becomes a lookup, and adding a ninth option no longer means editing a method that eight others depend on.
public interface IPedalMap { Torque Translate(double pedalPercent); }
public sealed class Accelerator
{
private IPedalMap _map;
public Accelerator(IPedalMap map) => _map = map;
public void SwitchTo(IPedalMap map) => _map = map; // swap at runtime
public Torque Press(double pedalPercent) => _map.Translate(pedalPercent);
}
// selection lives in one place, not scattered through the control loop
accelerator.SwitchTo(surface.IsSlippery ? _snow
: driver.WantsSport ? _sport
: _eco);
In .NET, the practical way is keyed DI: register each strategy with a key and ask for the one you need. The container becomes the lookup table, and each strategy can be tested on its own.
09State — the gear selector
One accelerator, four meanings. In Park, the pedal does nothing and the gearbox is locked. In Reverse, the car moves back and the camera comes on. In Neutral, the engine revs but the car does not move. In Drive, it goes. And the gear lever will not let you select Reverse at fifty kilometres an hour, whatever you do.
Try the gear lever. The same action gives different results, and the safety lock refuses anything that would damage the gearbox:
public interface IGear
{
IGear Select(Selector to, Vehicle car);
}
public sealed class Drive : IGear
{
public IGear Select(Selector to, Vehicle car) => to switch
{
Selector.Neutral => new Neutral(),
Selector.Park when !car.IsRolling => new Park(),
Selector.Reverse when !car.IsRolling => new Reverse(),
_ => this // the interlock holds
};
}
Strategy and State have almost the same class diagram, which is why people mix them up. The difference is who is in control. A strategy is chosen from outside and stays until someone outside changes it. A state replaces itself from inside, as events happen.
Use State when you have an order status, a claim workflow, a device lifecycle, anything where switch (status) appears in four methods and each one is missing a different case. Once each status is its own class, the compiler helps you: a new state that forgets to handle an action will not build.
04 — CHOOSINGWhich shelf to reach for
Patterns are names for shapes that good solutions end up taking. They are not where you start. Writing the simple version first is the right choice almost every time. The need for a pattern comes later, as a specific complaint about the code. Match the complaint, not the diagram.
| What you hear yourself saying | Shelf |
|---|---|
| "This class uses an exact type it should not even know about." | Factory Method |
| "The constructor has eleven parameters and six of them are optional." | Builder |
| "Having two of these at the same time would be a bug." | Singleton (via DI) |
| "The interface is wrong, and the code behind it is not mine to change." | Adapter |
| "I need to add logging, retries and caching without touching this class." | Decorator |
| "These five calls always happen together, in this order, in four places." | Facade |
| "One event, many unrelated reactions, and the list keeps growing." | Observer |
| "A switch that picks between algorithms that do the same job." | Strategy |
| "A switch on a status field that changes what every other method does." | State |
The rest of the list lives in the same workshop, and most of it maps just as neatly:
The value of a pattern name is this: when you say "make that a decorator" in a review, four people picture the same brake pedal with four modules behind it.
You will not need all twenty-three; very little software does. What the list gives you is a shared vocabulary, and an eye that spots trouble before the code gets bad: the constructor that keeps growing, the switch that has now appeared in a fourth method, the class that uses an exact type it should never have known about. Learn the nine on this line, and the other fourteen are a twenty-minute read on the day you need one.
Discussion