.NET Academy

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

PatternIn .NET
BuilderWebApplication.CreateBuilder(), StringBuilder, HostBuilder
IteratorIEnumerable<T>, yield return
SingletonServices registered with AddSingleton (prefer DI over static singletons)
Chain of responsibilityMiddleware pipeline, DelegatingHandler
MediatorMediatR (next lesson)
OptionsIOptions<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.

Program.cs
Loading editor…

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 pass
  1. 1.What is a design pattern?

  2. 2.Which pattern adds behavior to an object by wrapping it in another object with the same interface?

  3. 3.What does the Repository pattern do?

  4. 4.Which pattern does `IEnumerable<T>` with foreach implement?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)