Skip to content
Happy Programming Guide
Start learning
C#

C# Design Patterns

The design patterns that earn their place in C#: strategy, factory, repository, decorator, observer and options — with the language features that make some obsolete.

Hands typing on a laptop keyboard

A design pattern is a named solution to a problem that keeps recurring. In C# several classic patterns are already built into the language or the framework, so knowing which ones still need writing is more useful than memorising all twenty-three. This guide covers six that earn their place, and names the ones that no longer do.

Strategy#

Swap an algorithm without changing the code that uses it. This is the pattern C# uses most, usually without anyone naming it:

CSHARP
public interface IDiscountPolicy
{
    decimal Apply(decimal price);
}

public class NoDiscount : IDiscountPolicy
{
    public decimal Apply(decimal price) => price;
}

public class PercentageDiscount : IDiscountPolicy
{
    private readonly decimal _percent;
    public PercentageDiscount(decimal percent) => _percent = percent;

    public decimal Apply(decimal price) => price * (1 - _percent / 100m);
}

public class Checkout
{
    private readonly IDiscountPolicy _policy;
    public Checkout(IDiscountPolicy policy) => _policy = policy;

    public decimal Total(IEnumerable<decimal> prices) =>
        prices.Sum(_policy.Apply);
}

For a single method, a delegate is often enough and needs no interface at all:

CSHARP
public class Checkout
{
    private readonly Func<decimal, decimal> _discount;
    public Checkout(Func<decimal, decimal> discount) => _discount = discount;
}

var checkout = new Checkout(price => price * 0.9m);

Factory#

Create objects without the caller knowing which concrete type it gets:

CSHARP
public interface IExporter
{
    string Export(IEnumerable<Order> orders);
}

public class ExporterFactory
{
    private readonly IServiceProvider _services;
    public ExporterFactory(IServiceProvider services) => _services = services;

    public IExporter Create(string format) => format.ToLowerInvariant() switch
    {
        "csv"  => _services.GetRequiredService<CsvExporter>(),
        "json" => _services.GetRequiredService<JsonExporter>(),
        _      => throw new ArgumentException($"Unknown format: {format}"),
    };
}

Use a factory when the type is chosen at run time from data. When it is known at compile time, plain constructor injection is simpler.

Repository#

CSHARP
public interface IOrderRepository
{
    Task<Order?> GetAsync(int id);
    Task<IReadOnlyList<Order>> FindPaidAsync();
    Task AddAsync(Order order);
}

The case for it: your business logic no longer mentions the database, which makes it testable with an in-memory implementation.

The case against it, which is worth taking seriously: Entity Framework’s DbContext is already a unit of work and DbSet is already a repository. Wrapping them often produces a thin layer that hides useful query capability and adds a file per entity for no benefit.

Decorator#

Add behaviour to something without changing it. This is where the pattern is genuinely at its best:

CSHARP
public class CachingProductService : IProductService
{
    private readonly IProductService _inner;
    private readonly IMemoryCache _cache;

    public CachingProductService(IProductService inner, IMemoryCache cache)
    {
        _inner = inner;
        _cache = cache;
    }

    public async Task<Product?> GetAsync(int id)
    {
        if (_cache.TryGetValue(id, out Product? cached)) return cached;

        var product = await _inner.GetAsync(id);
        if (product is not null)
            _cache.Set(id, product, TimeSpan.FromMinutes(5));

        return product;
    }
}

Caching, logging, retries and metrics all fit this shape. Each decorator does one thing, and they stack — a logging decorator wrapping a caching decorator wrapping the real service. The core class stays unaware of all of it.

Observer#

C# has this in the language as events:

CSHARP
public class OrderService
{
    public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;

    public void Place(Order order)
    {
        Save(order);
        OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(order));
    }
}

service.OrderPlaced += (sender, e) => SendConfirmation(e.Order);
service.OrderPlaced += (sender, e) => UpdateStock(e.Order);

The null-conditional call is not optional — invoking an event with no subscribers throws. For anything crossing process boundaries, a message queue is the right version of this idea.

Options#

Not one of the classic twenty-three, but the pattern you will use most in .NET:

CSHARP
public class EmailOptions
{
    public const string Section = "Email";

    public string SmtpHost { get; set; } = "";
    public int Port { get; set; } = 587;
    public string FromAddress { get; set; } = "";
}

// Registration
builder.Services.Configure<EmailOptions>(
    builder.Configuration.GetSection(EmailOptions.Section));

// Use
public class EmailSender
{
    private readonly EmailOptions _options;

    public EmailSender(IOptions<EmailOptions> options) => _options = options.Value;
}

Configuration becomes a typed object rather than string lookups scattered through the code, and a typo in a key surfaces at start-up rather than at the moment you send an email.

Patterns C# has absorbed#

Pattern Use instead
Singleton DI container with singleton lifetime
Iterator IEnumerable and yield return
Observer events, or IObservable
Command Action and Func delegates
Template method a delegate parameter
Adapter often an extension method

Choosing when to use one#

Patterns are a response to a problem you already have, not a way to structure code you are about to write. Three questions worth asking:

  • Is the thing I am abstracting actually going to vary? An interface with one implementation that never gains a second is pure overhead.
  • Would a plain function do? A pattern with one method is usually a delegate.
  • Can I name the problem? “I need to swap the discount rule per customer type” justifies strategy. “It seemed more professional” does not.

Questions people ask#

Do I need to learn all the Gang of Four patterns?

No. Knowing the names helps you read other people’s code and discuss designs. In modern C# perhaps eight see regular use, and several of the rest are solved by language features.

Is dependency injection a pattern?

It is a form of inversion of control — a class receives its dependencies rather than creating them. It is the foundation most other patterns here build on.

Is MVC a design pattern?

It is an architectural pattern, describing how a whole application is arranged rather than how a few classes collaborate. Different scale, same idea.

When is a pattern too much?

When explaining the design takes longer than the code it replaces, or when adding a feature means editing five files. Both are signs the abstraction is not paying for itself.

Where to go next#

C# testing, where these designs prove their worthRead next

Keep reading

C#

C# Events and Delegates

What a delegate is, how events build on them, Action and Func, and the two mistakes that cause memory leaks and null…

5 min read

C#

Distinct() Not Working on Objects in C#

Distinct removes duplicates by equality, and for classes equality means the same instance. Three fixes: DistinctBy, a record, or IEquatable, and which…

3 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *