Design patterns
Proven solutions to recurring design problems — Repository, Strategy, Factory, Decorator and more.
13 min read
You will learn
- Explain what design patterns are (and aren't)
- Recognize Strategy, Factory, Repository, Decorator, Adapter and Observer
- Implement a caching decorator
- Spot patterns already used throughout .NET
A shared vocabulary
Many design problems come up again and again: create objects without hard-coding their type, swap an algorithm at run time, add behavior without modifying a class. Design patterns are named solutions to these problems, catalogued in the classic "Gang of Four" book (1994) and many since. Knowing them lets you say "wrap it in a decorator" and be understood instantly.
You've already used several without the names. Here are the ones you'll meet most in .NET backends.
Strategy — interchangeable algorithms
Put each variant of an algorithm behind an interface and choose one at run time:
interface IPricingStrategy { decimal Price(Order order); }
class RegularPricing : IPricingStrategy { ... }
class BlackFridayPricing : IPricingStrategy { ... }
class Checkout(IPricingStrategy pricing)
{
public decimal Total(Order order) => pricing.Price(order);
}IComparer<T> passed to Sort, and the shipping rules from the SOLID exercise, are strategies.
Factory — centralize object creation
When choosing which class to create involves logic, put it in one place:
static class NotifierFactory
{
public static INotifier Create(Channel channel) => channel switch
{
Channel.Email => new EmailNotifier(),
Channel.Sms => new SmsNotifier(),
Channel.Push => new PushNotifier(),
_ => throw new ArgumentOutOfRangeException(nameof(channel)),
};
}IHttpClientFactory and ILoggerFactory are factories. Often, the DI container plays this role.
Repository — hide persistence
interface IOrderRepository
{
Task<Order?> GetAsync(int id);
Task AddAsync(Order order);
}Domain and application code talk to an interface; only the implementation knows about EF Core, SQL or an external API. EF Core's DbSet<T> is itself a repository and DbContext a unit of work, so many teams use DbContext directly in simple apps and add repositories when they need a clearer boundary.
Decorator — add behavior by wrapping
A decorator implements the same interface as the object it wraps and adds behavior before/after delegating:
class LoggingOrderRepository(IOrderRepository inner, ILogger<LoggingOrderRepository> log) : IOrderRepository
{
public async Task<Order?> GetAsync(int id)
{
log.LogInformation("Loading order {Id}", id);
return await inner.GetAsync(id);
}
public Task AddAsync(Order order) => inner.AddAsync(order);
}Decorators stack: retry → caching → logging → real repository, each unaware of the others. ASP.NET Core middleware and HttpClient handlers are decorator chains. You'll write a caching decorator in the exercise.
Adapter — make incompatible interfaces fit
Wrap a third-party SDK so the rest of your code sees your interface:
class StripePaymentGateway(StripeClient stripe) : IPaymentGateway
{
public async Task<bool> ChargeAsync(decimal amount, string token) =>
(await stripe.Charges.CreateAsync(new() { Amount = (long)(amount * 100), Source = token })).Paid;
}Switching providers later means writing a new adapter, not touching business logic.
Observer — notify subscribers
When something happens, interested parties are notified without the source knowing them: C# events (Track 4), IObservable<T>, and at the system level, pub/sub messaging (Track 7).
Other patterns you'll see
| Pattern | In .NET |
|---|---|
| Builder | WebApplication.CreateBuilder(), StringBuilder, HostBuilder |
| Iterator | IEnumerable<T>, yield return |
| Singleton | Services registered with AddSingleton (prefer DI over static singletons) |
| Chain of responsibility | Middleware pipeline, DelegatingHandler |
| Mediator | MediatR (next lesson) |
| Options | IOptions<T> |
Patterns are tools, not goals
Adding a factory, a strategy and three decorators to a 50-line feature makes it harder to understand. Reach for a pattern when you feel the problem it solves.
Key takeaways
- Patterns are named, reusable solutions — a shared vocabulary for design.
- Strategy swaps algorithms, Factory centralizes creation, Repository hides persistence.
- Decorator adds behavior by wrapping (and stacks); Adapter wraps foreign APIs in your interfaces.
- .NET itself is built on patterns — recognize them, and apply them only when they simplify.
Exercise
CountingRepository is the "real" repository (it counts how often it's called). Write CachingProductRepository — a decorator that implements the same interface, wraps another IProductRepository, and only calls it the first time each product id is requested.
C# runtime not loaded
Hint 1
Store the inner repository in a field, plus a Dictionary<int, string> cache.
Hint 2
GetName: if (!cache.TryGetValue(id, out var name)) { name = inner.GetName(id); cache[id] = name; } return name;
Show solution
var real = new CountingRepository();
IProductRepository repo = new CachingProductRepository(real);
repo.GetName(1); repo.GetName(1); repo.GetName(2);
Console.WriteLine($"Calls to the real repository: {real.Calls}");
interface IProductRepository
{
string GetName(int id);
}
// The "slow" real implementation — used by the checks, don't change.
class CountingRepository : IProductRepository
{
public int Calls { get; private set; }
public string GetName(int id)
{
Calls++;
return $"Product {id}";
}
}
class CachingProductRepository(IProductRepository inner) : IProductRepository
{
private readonly Dictionary<int, string> cache = new();
public string GetName(int id)
{
if (!cache.TryGetValue(id, out var name))
{
name = inner.GetName(id);
cache[id] = name;
}
return name;
}
}Quiz
4 questions · 3 correct to pass1.What is a design pattern?
2.Which pattern adds behavior to an object by wrapping it in another object with the same interface?
3.What does the Repository pattern do?
4.Which pattern does `IEnumerable<T>` with foreach implement?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)