Here are ten proven C# design/coding patterns
Here are ten proven C# design/coding patterns that help you write clear, maintainable, SOLID-compliant code. For each, you’ll find:
Intent
How it promotes SOLID
Minimal C# example
When to use
1. Dependency Injection (DI)
Intent: Decouple concrete implementations from consumers.
SOLID: ✔ Single Responsibility (clients don’t create dependencies)
✔ Dependency Inversion (depend on abstractions)
public interface IMessageSender
{
void Send(string msg);
}
public class EmailSender : IMessageSender
{
public void Send(string msg) => /* send email */;
}
public class NotificationService
{
private readonly IMessageSender _sender;
public NotificationService(IMessageSender sender) => _sender = sender;
public void Notify(string msg) => _sender.Send(msg);
}
// Composition root
var sender = new EmailSender(); var svc = new NotificationService(sender);When: Anytime a class needs collaborators, it makes testing and swapping implementations trivial.
2. Factory Method
Intent: Defer instantiation of concrete types to subclasses or dedicated factories.
SOLID: ✔ Open/Closed (new products without modifying client)
public abstract class LoggerFactory
{
public abstract ILogger CreateLogger();
}
public class FileLoggerFactory : LoggerFactory
{
public override ILogger CreateLogger() => new FileLogger("app.log");
}
public class App
{
private readonly ILogger _log;
public App(LoggerFactory factory) => _log = factory.CreateLogger();
}When: You need to create families of related objects without hard-coding types.
3. Strategy
Intent: Encapsulate interchangeable algorithms behind a common interface.
SOLID: ✔ Open/Closed (add new strategies without changing context)
✔ Single Responsibility (each algorithm in its own class)
public interface IPricingStrategy
{
decimal Calculate(Order o);
}
public class RegularStrategy : IPricingStrategy
{
public decimal Calculate(Order o) => o.Total;
}
public class DiscountStrategy : IPricingStrategy
{
public decimal Calculate(Order o) => o.Total * 0.9m;
}
public class OrderProcessor
{
private readonly IPricingStrategy _strategy;
public OrderProcessor(IPricingStrategy strat) => _strategy = strat;
public void Process(Order o)
{
var charge = _strategy.Calculate(o);
// charge customer…
}
}When: Business rules or algorithms vary and must be swappable at runtime or config time.
4. Repository
Intent: Abstract data access behind a collection-like interface.
SOLID: ✔ Interface Segregation (only expose needed operations)
✔ Dependency Inversion (depend on repository abstractions)
public interface IProductRepository
{
Product GetById(int id);
void Add(Product p);
IEnumerable<Product> ListAll();
}
public class SqlProductRepository : IProductRepository
{
private readonly DbContext _ctx;
public SqlProductRepository(DbContext ctx) => _ctx = ctx;
public Product GetById(int id) => _ctx.Products.Find(id);
public void Add(Product p) => _ctx.Products.Add(p);
public IEnumerable<Product> ListAll() => _ctx.Products.ToList();
}When: You want to keep business logic free of ORM or SQL specifics.
Note: Entity Framework is the basis of most ERM-based Repository patterns, as the Context object manages most of the requirements.
5. Unit of Work
Intent: Group several operations into a single transactional work unit.
SOLID: ✔ Single Responsibility (coordinate repositories/transactions)
public interface IUnitOfWork : IDisposable
{
IProductRepository Products { get; }
IOrderRepository Orders { get; }
void Commit();
}
public class EfUnitOfWork : IUnitOfWork
{
private readonly DbContext _ctx;
public IProductRepository Products { get; }
public IOrderRepository Orders { get; }
public EfUnitOfWork(DbContext ctx)
{
_ctx = ctx; Products = new SqlProductRepository(ctx);
Orders = new SqlOrderRepository(ctx);
}
public void Commit() => _ctx.SaveChanges();
public void Dispose() => _ctx.Dispose();
}When: You need to ensure multiple repository operations succeed or fail as one.
6. Template Method
Intent: Define the skeleton of an algorithm in a base class while deferring steps to subclasses.
SOLID: ✔ Open/Closed (base algorithm stays fixed; steps vary)
public abstract class DataExporter
{
public void Export()
{
var data = ReadData();
var formatted = Format(data);
Write(formatted);
}
protected abstract IEnumerable<object> ReadData();
protected abstract string Format(IEnumerable<object> data);
protected abstract void Write(string content);
}
public class CsvExporter : DataExporter
{
protected override IEnumerable<object> ReadData() => /* Do Something */;
protected override string Format(IEnumerable<object> d) => /* CSV data */;
protected override void Write(string c) => File.WriteAllText("out.csv", c);
}When: You have a fixed workflow with customizable steps.
7. Decorator
Intent: Add behavior to objects dynamically without subclassing.
SOLID: ✔ Open/Closed (wrap to add behavior)
public interface IMessage
{
string GetText();
}
public class SimpleMessage : IMessage
{
public string GetText() => "Hello";
}
public class HtmlMessageDecorator : IMessage
{
private readonly IMessage _inner;
public HtmlMessageDecorator(IMessage inner) => _inner = inner;
public string GetText() => $"<b>{_inner.GetText()}</b>";
}When: You need to layer responsibilities (e.g., logging, validation, formatting) transparently.
8. Adapter
Intent: Make one interface work with another by translating calls.
SOLID: ✔ Single Responsibility (adapter isolates translation logic)
// Legacy interface
public class LegacyPrinter
{
public void Print(string s)
{
/* Do something*/
}
}
// Target interface
public interface IPrinter
{
void PrintLine(string line);
}
// Adapter
public class PrinterAdapter : IPrinter
{
private readonly LegacyPrinter _legacy;
public PrinterAdapter(LegacyPrinter legacy) => _legacy = legacy;
public void PrintLine(string line) => _legacy.Print(line + "\n");
}When: You must integrate with existing or third-party APIs that don’t match your contracts.
9. Observer (Event Aggregator)
Intent: Decouple publishers from subscribers via a common event channel.
SOLID: ✔ Dependency Inversion (both depend on event abstractions)
public class EventBus
{
public event Action<Order> OrderPlaced;
public void PublishOrder(Order o) => OrderPlaced?.Invoke(o);
}
public class BillingService
{
public BillingService(EventBus bus) => bus.OrderPlaced += OnOrder;
private void OnOrder(Order o) => /* bill customer */;
}When: Many components need to react to domain events without tight coupling.
10. Specification
Intent: Encapsulate business rules or filters in reusable predicates.
SOLID: ✔ Single Responsibility (rule logic in one place)
✔ Open/Closed (combine or extend specs)
public interface ISpec<T>
{
bool IsSatisfiedBy(T t);
}
public class ActiveUserSpec : ISpec<User>
{
public bool IsSatisfiedBy(User u) => u.IsActive;
}
public class PremiumUserSpec : ISpec<User>
{
public bool IsSatisfiedBy(User u) => u.IsPremium;
}
// Combine specs
public class AndSpec<T> : ISpec<T>
{
private readonly ISpec<T> _a, _b;
public AndSpec(ISpec<T> a, ISpec<T> b) => (_a,_b)=(a,b);
public bool IsSatisfiedBy(T t) => _a.IsSatisfiedBy(t)
&& _b.IsSatisfiedBy(t);
}When: You have complex, combinable business rules or query predicates.
Final Tips
Favor small, focused classes and interfaces.
Program to interfaces, not implementations.
Keep dependencies injected, not instantiated.
Encapsulate one responsibility per module.
Apply these patterns judiciously—each brings clarity, testability, and adherence to SOLID principles.

