Overview Intermediate 9 min read

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
  1. Where do I start with three questions, then a pattern?
  2. How does the decision tree look by symptom?
  3. Object creation symptoms
  4. Composition symptoms
  5. Behaviour symptoms
  6. Which confusion matrices come up most in interviews?
  7. Strategy vs State vs Command
  8. Decorator vs Adapter vs Proxy
  9. Factory Method vs Abstract Factory vs Builder
  10. Mediator vs Observer
  11. Composite vs Decorator vs Iterator vs Visitor
  12. What do the modern .NET shapes look like at a glance?
  13. When should you NOT use any pattern?
  14. How do you read PR reviews after this series?
  15. 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>

→ See Mediator, Observer.

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:

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:

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.

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?
Memorise the eight you meet weekly: Singleton, Factory Method, Strategy, Decorator, Adapter, Observer, Iterator, Command. Recognise the rest by reading them once. The map matters more than the dictionary.
What is the fastest way to choose between Strategy, State, and Command?
Ask one question: who picks? If the caller picks one of several algorithms, you want Strategy. If the object's lifecycle picks (Created → Paid → Shipped), you want State. If the caller wraps the action as data to queue, log, or undo, you want Command.
When does a pattern indicate the wrong abstraction?
When the pattern fights modern .NET features. Every Singleton you write by hand instead of 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.
What about anti-patterns — when is using a pattern itself a mistake?
When you reach for a pattern after learning it (Maslow's hammer), when you wrap a single concrete class in a one-implementation interface, when the pattern adds files without unlocking new behaviour. The cost of every pattern is real and pays back only at scale. Three rule of thumb: wait until the third time you see the same problem before reaching for the pattern.