How to Choose the Right Design Pattern in C#
Choose the right design pattern in C# / .NET 10 by matching problem symptoms to intent: a decision tree across all 23 GoF patterns with cross-links.
Table of contents
- Where do I start with three questions, then a pattern?
- How does the decision tree look by symptom?
- Object creation symptoms
- Composition symptoms
- Behaviour symptoms
- Which confusion matrices come up most in interviews?
- Strategy vs State vs Command
- Decorator vs Adapter vs Proxy
- Factory Method vs Abstract Factory vs Builder
- Mediator vs Observer
- Composite vs Decorator vs Iterator vs Visitor
- What do the modern .NET shapes look like at a glance?
- When should you NOT use any pattern?
- How do you read PR reviews after this series?
- Where should you read next in this series?
You have read 23 patterns. You have a problem in front of you. Which one do you reach for? This article is the bridge between the patterns and the daily work — a decision tree that maps real-world symptoms to the right pattern, plus the comparison matrices that resolve the four or five "wait, which one was that?" pairings that show up in every code review.
Read this once when you finish the series. Bookmark it for the next time you stare at a refactor and ask "which pattern should this be?".
Where do I start with three questions, then a pattern?
Every design-pattern decision in modern C# fits into a tree three levels deep. Ask in order:
flowchart TB
Q1{What is the problem about?}
Q1 -->|How an object is created| Creational
Q1 -->|How objects are wired together| Structural
Q1 -->|How objects act and talk| Behavioral
Creational --> Q2A{What kind of creation?}
Q2A -->|Share one process-wide instance| Singleton[Singleton]
Q2A -->|Pick which concrete class| FactMth[Factory Method]
Q2A -->|Pick a family of related classes| AbsFac[Abstract Factory]
Q2A -->|Many optional fields, validation| Bld[Builder]
Q2A -->|Clone a configured template| Proto[Prototype]
Structural --> Q2B{What kind of wiring?}
Q2B -->|Mismatched interfaces| Ada[Adapter]
Q2B -->|Two axes that vary together| Bri[Bridge]
Q2B -->|Tree of leaves and groups| Comp[Composite]
Q2B -->|Add cross-cutting behaviour| Dec[Decorator]
Q2B -->|Hide a complex subsystem| Fac[Facade]
Q2B -->|Share immutable instances| Fly[Flyweight]
Q2B -->|Control access lazily / remotely| Pxy[Proxy]
Behavioral --> Q2C{What kind of communication?}
Q2C -->|Pipeline that may stop| CoR[Chain of Responsibility]
Q2C -->|Wrap an action as data| Cmd[Command]
Q2C -->|Evaluate a small DSL| Int[Interpreter]
Q2C -->|Iterate without exposing structure| Iter[Iterator]
Q2C -->|Many peers via a hub| Med[Mediator]
Q2C -->|Snapshot for undo| Mem[Memento]
Q2C -->|Notify subscribers| Obs[Observer]
Q2C -->|Behaviour by lifecycle| Sta[State]
Q2C -->|Swap one algorithm| Str[Strategy]
Q2C -->|Skeleton with hooks| Tmp[Template Method]
Q2C -->|New ops on closed nodes| Vis[Visitor]
Every leaf is a chapter you have already read. The next sections narrow the choice when two patterns look similar.
How does the decision tree look by symptom?
Below is the same tree expressed as "you have X symptom → read Y chapter". Skim it before you write any new code; the five minutes you spend here save the half-day refactor later.
Object creation symptoms
| Symptom | Pattern |
|---|---|
"I am calling new for the same object 1,000 times per second." |
Singleton |
"I have a switch over a string deciding which class to instantiate." |
Factory Method |
| "I need US dollar inputs to come with US address forms — never EU ones." | Abstract Factory |
| "My constructor has 12 parameters, half optional." | Builder |
| "I have one configured object and need 11 near-copies." | Prototype |
Composition symptoms
| Symptom | Pattern |
|---|---|
| "The legacy SDK does not match my interface." | Adapter |
| "I have N styles × M channels = NM classes." | Bridge |
| "Same operation must work on a single item and a group." | Composite |
| "I want to add logging / retry / caching around an existing call." | Decorator |
| "My controller has seven injected services." | Facade |
| "A million identical small objects are killing memory." | Flyweight |
| "I want to defer or guard access to an expensive object." | Proxy |
Behaviour symptoms
| Symptom | Pattern |
|---|---|
| "Each rule may pass, fail, or short-circuit a request." | Chain of Responsibility |
| "Each user action must be queueable, replayable, or undoable." | Command |
| "Non-developers must author rules in a string." | Interpreter |
| "Walk a collection without revealing its structure." | Iterator |
| "Many peers must coordinate without holding references." | Mediator |
| "Save state before a risky operation; restore on cancel." | Memento |
| "Tell several modules when something happened." | Observer |
| "An object's allowed actions depend on its lifecycle stage." | State |
| "The caller picks one algorithm of several." | Strategy |
| "Many subclasses share 80% of an algorithm." | Template Method |
| "Add new operations to a closed tree without editing nodes." | Visitor |
Which confusion matrices come up most in interviews?
These are the half-dozen "which one is this?" pairs that come up over and over.
Strategy vs State vs Command
The shape is identical: an interface implemented by several classes, picked at runtime. The intent is what differs.
| Question | Strategy | State | Command |
|---|---|---|---|
| Who picks? | Caller | Object's own lifecycle | Caller (sender) |
| What is picked? | One algorithm | Whole behaviour | One action (with inputs) |
| Lifetime | Long (DI-injected) | Tied to object lifecycle | Short (one execution) |
| Modern .NET | Func<>, DI |
State machine, Stateless | IRequest<T> + handler (MediatR) |
→ See Strategy, State, Command.
Decorator vs Adapter vs Proxy
All three wrap one object behind a class.
| Question | Decorator | Adapter | Proxy |
|---|---|---|---|
| Same interface as wrapped? | Yes | No (changes interface) | Yes |
| Adds behaviour? | Yes | No | No (or transparent) |
| Controls access? | No | No | Yes |
→ See Decorator, Adapter, Proxy.
Factory Method vs Abstract Factory vs Builder
Three creational patterns that all involve "make stuff".
| Question | Factory Method | Abstract Factory | Builder |
|---|---|---|---|
| Returns | One product | A family of products | One complex product |
| Number of axes | One | Many, varying together | Zero (just optional fields) |
| Modern .NET | AddKeyedScoped<> |
DI per scope | record + optional fluent class |
→ See Factory Method, Abstract Factory, Builder.
Mediator vs Observer
Both push notifications.
| Question | Mediator | Observer |
|---|---|---|
| Hub class | Required | None |
| Subscriber visibility | Subscriber registers with hub | Subscriber registers with publisher |
| Modern .NET | MediatR IMediator.Publish |
event, IObservable<T>, Channel<T> |
Composite vs Decorator vs Iterator vs Visitor
These four come up around tree-shaped data.
| Question | Composite | Decorator | Iterator | Visitor |
|---|---|---|---|---|
| Reference count | Many children | One | One source | Receives the whole tree |
| What it adds | Tree shape | Behaviour | Sequential traversal | A new operation |
| Modern .NET | Plain interface | Scrutor Decorate |
yield, IAsyncEnumerable |
switch patterns |
→ See Composite, Decorator, Iterator, Visitor.
What do the modern .NET shapes look like at a glance?
The whole series is built around a single observation: every GoF pattern's textbook implementation has shrunk. The implementation maps to modern features:
| Pattern | Modern .NET shape |
|---|---|
| Singleton | services.AddSingleton<T>() |
| Factory Method | services.AddKeyedScoped<TInterface, T>(key) |
| Abstract Factory | DI registration per region/scope |
| Builder | record + init-only setters + optional fluent class |
| Prototype | record + with expression |
| Adapter | One wrapper class per foreign type |
| Bridge | Two interfaces injected separately |
| Composite | One interface implemented by leaf and group records |
| Decorator | services.Decorate<>() (Scrutor) or middleware |
| Facade | Application service / use-case class |
| Flyweight | static readonly instances or content-keyed cache |
| Proxy | EF Core lazy proxies, gRPC clients, hand-rolled wrappers |
| Chain of Responsibility | ASP.NET Core middleware, MediatR pipeline behaviours |
| Command | IRequest<TResult> + IRequestHandler<,> (MediatR) |
| Interpreter | Expression<Func<>> + Compile() |
| Iterator | yield return and IAsyncEnumerable<T> |
| Mediator | MediatR IMediator.Publish / Send |
| Memento | record snapshot + with |
| Observer | event, IObservable<T>, INotifyPropertyChanged, Channel<T> |
| State | enum + switch, or Stateless |
| Strategy | Func<> or injected interface |
| Template Method | Abstract base with protected virtual hooks (BackgroundService) |
| Visitor | switch expression with type patterns |
If your code looks nothing like the row to the right, you are probably writing the 1994 implementation when the 2026 one exists. The intent survives; the implementation does not.
When should you NOT use any pattern?
The most over-applied advice in this series is "use a pattern". The most under-applied advice is "do not". Three signs that no pattern is the right answer:
- You have one variant. A
Strategyinterface with one implementation is overhead. Wait for the second. - The framework already does it. ASP.NET Core's middleware is Chain of Responsibility; you do not need a hand-rolled one to validate a request.
- The cost outweighs the benefit. Three extra files for an abstraction nobody uses is a tax with no revenue. Patterns have to earn their existence.
The rule of three (referenced in the Introduction): wait until you have three concrete cases of the same problem before reaching for the pattern. Two is a coincidence; three is a pattern.
How do you read PR reviews after this series?
Once you know all 23 patterns, code reviews become a vocabulary exercise rather than a guessing game. Common review phrases that should now make immediate sense:
- "This is a Strategy that should be a State." — the chosen algorithm depends on the object's lifecycle, not the caller.
- "This is a Decorator that wants to be a Proxy." — the wrapper is controlling access, not adding behaviour.
- "This is a Facade with too many methods." — the application service has become a god class.
- "This Builder should be a record." — the construction has no staged validation, just optional fields.
- "This Visitor should be a
switchexpression." — the node hierarchy is closed and you control all cases.
When you can both give and receive these reviews fluently, the patterns have done their job: they are vocabulary for a conversation that used to be a 30-minute meeting.
Where should you read next in this series?
- Previous: Visitor — the last GoF pattern.
- Next: Conclusion — the wrap-up and what to read after the series.
- Series map: Introduction.
- Group outlines (internal): scan a single group when you need a refresher on its patterns and cross-references.
A final structural note. Every pattern in this series has the same seven sections: symptom, textbook shape, modern .NET shape, when to skip, comparison, real example, where next. That uniformity is deliberate — once you internalise the structure, re-reading any chapter takes five minutes. Treat the series as a reference, not a one-time read. The patterns matter most when you can find the right one in 30 seconds, not when you remember them all by heart.
For a quick code-level reminder, the most-frequent transformation this series taught is "lift inline behaviour into a swappable abstraction":
// Before — inline switch hard-codes the choice
public decimal Discount(string tier) => tier switch
{
"silver" => 0.05m, "gold" => 0.10m, _ => 0m,
};
// After — Strategy via Func<>, plus DI registration
public sealed class CartCalculator
{
private readonly Func<string, decimal> _discount;
public CartCalculator(Func<string, decimal> discount) => _discount = discount;
public decimal GrandTotal(string tier, decimal subtotal) => subtotal * (1m - _discount(tier));
}
That single transformation is the seed of half the patterns in this series. Recognise the seed and the rest follow.
Frequently asked questions
Should I memorise all 23 design patterns?
What is the fastest way to choose between Strategy, State, and Command?
When does a pattern indicate the wrong abstraction?
AddSingleton<T>(). Every hand-rolled Iterator instead of yield return. Every Memento class instead of record + with. Patterns are timeless ideas; the moment you are working against the language, the implementation is wrong even if the intent is right.