Overview Beginner 8 min read

Design Patterns in C# — A Beginner's Guide That Sticks

A beginner-friendly tour of design patterns in C# / .NET 10: what they are, when to use them, and how to read the 23 GoF classics without the jargon.

Table of contents
  1. What are design patterns in C# / .NET?
  2. Why do design patterns still matter in 2026?
  3. How are the 23 Gang of Four patterns grouped?
  4. How do you read a design pattern description without getting lost?
  5. When should you avoid forcing a pattern into your code?
  6. What is a tiny "before and after" example I can see right now?
  7. Where should you go next in this series?

You have seen the symptom. A method that started life as ten readable lines is now a 200-line if / else ladder full of if (request.Type == "X") branches. Every new feature adds another branch, every bug fix touches three of them, and the test file is two thousand lines long. The code works. Nobody enjoys opening it.

Most developers reach this point and Google "how to refactor big switch statement". They find a result that mentions "the Strategy pattern" — and they leave more confused than when they started, because the explanation was a UML diagram with ConcreteStrategyA and ConcreteStrategyB on it. This series exists to fix that. We will work through every Gang of Four pattern with C# .NET 10 code, real symptoms, and zero AbstractFactoryFactory filler.

This first article is the map. By the end you will know what a design pattern is, how to read one, when not to use one, and where to start in the rest of the series.

What are design patterns in C# / .NET?

A design pattern is a named, reusable solution to a problem that keeps showing up across object-oriented codebases. Three things make it a pattern rather than just "a piece of advice":

  1. It has a name. "Strategy" is a word two senior engineers can use in a PR review and instantly agree what they mean. The name is half the value.
  2. It has a shape. The pattern describes the participating roles (interfaces, classes, methods) and how they collaborate. You can draw it.
  3. It has trade-offs. A pattern lists what it costs you — extra files, indirection, performance — so you can decide whether the benefit is worth it.

In .NET, you have already used dozens of patterns without naming them. IEnumerable<T> is the Iterator pattern. ASP.NET Core middleware is the Chain of Responsibility pattern. IOptions<T> is part Strategy, part Adapter. EF Core's lazy loading is the Proxy pattern. The framework authors learnt these names so the docs would fit on one page; once you learn them too, the docs become much shorter.

Why do design patterns still matter in 2026?

The honest answer: not for the reasons your university textbook gave you. They matter because of communication, recognition, and refactoring leverage.

Modern C# does change how patterns look. Records make immutable Memento trivial. Source generators replace some Visitor implementations. Func<T, TResult> collapses many Command and Strategy classes into a single line. The intents survive; the boilerplate shrinks.

How are the 23 Gang of Four patterns grouped?

In 1994, four authors (Gamma, Helm, Johnson, Vlissides — the "Gang of Four", often abbreviated GoF) published the book that named these patterns and sorted them into three families:

flowchart TB
    GOF["23 GoF Design Patterns"]
    GOF --> CRE["Creational · 5 patterns<br>how objects are made"]
    GOF --> STR["Structural · 7 patterns<br>how objects are composed"]
    GOF --> BEH["Behavioral · 11 patterns<br>how objects communicate"]

    CRE --> C1[Singleton]
    CRE --> C2[Factory Method]
    CRE --> C3[Abstract Factory]
    CRE --> C4[Builder]
    CRE --> C5[Prototype]

    STR --> S1[Adapter]
    STR --> S2[Bridge]
    STR --> S3[Composite]
    STR --> S4[Decorator]
    STR --> S5[Facade]
    STR --> S6[Flyweight]
    STR --> S7[Proxy]

    BEH --> B1[Chain of Responsibility]
    BEH --> B2[Command]
    BEH --> B3[Interpreter]
    BEH --> B4[Iterator]
    BEH --> B5[Mediator]
    BEH --> B6[Memento]
    BEH --> B7[Observer]
    BEH --> B8[State]
    BEH --> B9[Strategy]
    BEH --> B10[Template Method]
    BEH --> B11[Visitor]

The grouping is more useful than it looks:

If you can place a problem in one of those three buckets, you have already narrowed your search from 23 patterns to about 7. That alone is worth the five minutes it takes to memorise the diagram above.

How do you read a design pattern description without getting lost?

Pattern descriptions follow a fixed shape. Once you know the shape, even unfamiliar patterns become skimmable. Every chapter in this series uses the same headings on purpose:

Section What it answers
Intent The one-sentence "what problem does this solve".
Structure The class diagram — who knows about whom.
When to use The concrete symptoms in your code.
When NOT to use The traps — usually "I just learnt this".
C# example A runnable Program.cs with output.
Comparison How it differs from the most-confused sibling pattern.
Pitfalls What goes wrong in production.

When you read a new pattern, read in this order: Intent → When to use → C# example → Pitfalls. Skip Structure on the first pass. The diagram makes sense only after you have seen the code; reading it first is the main reason beginners think patterns are abstract nonsense.

When should you avoid forcing a pattern into your code?

This is the section nobody puts first, and it is the one you need most.

Rule of three. A pattern earns its complexity on the third time you see the same problem, not the first. If only one thing varies right now, a single if is the right answer. Two? Probably still an if. Three? Now you can plausibly extract a Strategy or a Factory.

Maslow's hammer. Two weeks after learning Visitor, you will see Visitor problems everywhere. You will not actually have Visitor problems everywhere. Wait for the symptoms — the actual switch ladder, the actual duplicated clone code — before applying the pattern.

The cost is real. Every pattern adds files, names, and indirection. The person who reads your code six months from now (often you) pays that cost on every visit. If the pattern is not buying you something concrete — the ability to add a feature without modifying existing code, isolation for unit tests, removal of a real duplication — it is a tax with no revenue.

Frameworks already encode many patterns. ASP.NET Core's pipeline is Chain of Responsibility. Dependency injection containers are Service Locator

What is a tiny "before and after" example I can see right now?

Here is the symptom we opened with — a price calculator that grew an if ladder for every customer tier. The before code is what the team checks in on a Tuesday afternoon under deadline pressure:

// Before — every new tier means another branch + another bug surface.
public decimal CalculateDiscount(Customer customer, decimal subtotal)
{
    if (customer.Tier == "Standard")  return subtotal * 0.00m;
    if (customer.Tier == "Silver")    return subtotal * 0.05m;
    if (customer.Tier == "Gold")      return subtotal * 0.10m;
    if (customer.Tier == "Platinum")  return subtotal * 0.15m + 5m;
    if (customer.Tier == "Employee")  return subtotal * 0.30m;
    return 0m;
}

The Strategy pattern says "pull each branch out, give it a name, and let the caller pick one". In modern C#, that does not even need new classes:

// After — one line per rule, additions are isolated, the dispatcher is dumb.
using DiscountRule = System.Func<decimal, decimal>;

public static class DiscountRules
{
    public static readonly Dictionary<string, DiscountRule> ByTier = new()
    {
        ["Standard"] = sub => 0m,
        ["Silver"]   = sub => sub * 0.05m,
        ["Gold"]     = sub => sub * 0.10m,
        ["Platinum"] = sub => sub * 0.15m + 5m,
        ["Employee"] = sub => sub * 0.30m,
    };
}

public decimal CalculateDiscount(Customer customer, decimal subtotal)
    => DiscountRules.ByTier.TryGetValue(customer.Tier, out var rule)
        ? rule(subtotal)
        : 0m;

The "after" version is the Strategy pattern, even though there is no IDiscountStrategy interface. The point of the pattern is to make the varying piece replaceable, not to introduce specific class shapes. C#'s Func<> is enough. When the rules grow stateful — they need to log, they need to call other services, they need to be unit-tested in isolation — that is when you graduate to a real interface. And when you do, you already know what to call it.

We will return to this exact example, with progressively more realistic constraints, in the dedicated chapter on the Strategy pattern.

Where should you go next in this series?

The series is ordered to be read top-to-bottom, but you can also jump in by problem. Three suggested paths:

A final note on how to read each chapter: every article in this series has a Quick Answer block right under the title. If you only have 30 seconds — in a meeting, mid-PR — read the Quick Answer and the FAQ. Come back for the code when you have time. That structure is deliberate; design patterns should make your reading faster, not slower.

Frequently asked questions

Are design patterns still relevant in modern C# with records, pattern matching, and minimal APIs?
Yes — but their shape changes. Records make Memento and Prototype almost free. Func<> and lambdas often replace boilerplate Strategy and Command classes. The intents still apply; the implementations get shorter. Knowing the pattern lets you recognise when a one-line Func<> is enough and when you actually need a class hierarchy.
Do I need to memorise all 23 Gang of Four patterns?
No. Memorise the eight you meet weekly — Singleton, Factory Method, Builder, Adapter, Decorator, Observer, Strategy, Iterator — and learn to recognise the rest. When a problem feels like one you have seen before, you will know which chapter of this series to re-read. That recognition skill is the actual goal.
What is the difference between a design pattern, a SOLID principle, and an architecture style?
Principles (SOLID, DRY) are rules you apply at every line of code. Patterns are named solutions to a class of problem at the level of a few classes. Architecture styles (microservices, hexagonal, MVC) are how whole systems are organised. You can violate a principle without breaking a pattern, and vice versa.
Should I refactor existing code to use a pattern just because I recognise one?
Almost never. A pattern only earns its complexity when the code has at least two reasons to change in the same direction. Forcing Strategy on a single if/else makes the code harder to read for everyone who comes after you. Wait for the second case before reaching for the pattern.