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
- What are design patterns in C# / .NET?
- Why do design patterns still matter in 2026?
- How are the 23 Gang of Four patterns grouped?
- How do you read a design pattern description without getting lost?
- When should you avoid forcing a pattern into your code?
- What is a tiny "before and after" example I can see right now?
- 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":
- 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.
- It has a shape. The pattern describes the participating roles (interfaces, classes, methods) and how they collaborate. You can draw it.
- 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.
- Communication. "Let's wrap that legacy SDK in an Adapter and put a Decorator around it for retries" is a one-sentence design discussion. The same idea explained from scratch is a 30-minute meeting.
- Recognition. When you can name 23 shapes of common problems, you stop redesigning the wheel. You see the shape, walk to the right toolbox, and pick up the right tool. Beginners debate; experts recognise.
- Refactoring leverage. Most patterns describe a direction you can move
bad code in. Realising your tangled
switchwants to be a Strategy gives you a target for an incremental refactor. You no longer change ten things at once; you change one thing toward a known endpoint.
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:
- Creational patterns answer the question who creates the object, and
with what knowledge? When you find yourself saying "I need an X but I do
not want to call
new X()here", a creational pattern is probably the answer. - Structural patterns answer how do these classes fit together without being glued shut? They are about wiring without welding.
- Behavioral patterns answer how do these objects pass work between each other at runtime? They are mostly about who decides what, and when.
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
- Abstract Factory. MediatR is Mediator + Command. If the framework already gives you the pattern, you do not need to roll your own; you need to learn the framework's API.
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:
- "I have one weekend to skim the field." Read this article, then Singleton (the most over-used pattern, so worth understanding deeply), then Factory Method, Strategy, and Observer. That is the 80% you will meet most weeks.
- "I have a specific problem right now." Open How to choose the right design pattern and follow the decision tree there.
- "I want the full GoF tour." Just keep reading — the next article is Singleton.
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?
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?
What is the difference between a design pattern, a SOLID principle, and an architecture style?
Should I refactor existing code to use a pattern just because I recognise one?
if/else makes the code harder to read for everyone who comes after you. Wait for the second case before reaching for the pattern.