Advanced C# Design Patterns: Scalable Architecture, Complex Patterns, and Real-World Applications
Advanced design patterns focus on application architecture, scalability, and maintainability at scale. These patterns are commonly used in enterprise systems and often work together rather than in isolation.
At this level, the goal is not just solving isolated problems, but designing entire systems that are robust, testable, and adaptable.
Selected Advanced Design Patterns
We’ll cover:
• Dependency Injection (DI)
• Mediator Pattern
• CQRS (Command Query Responsibility Segregation)
• Unit of Work Pattern
1. Dependency Injection (DI)
Purpose: Provides dependencies to a class from the outside, rather than creating them internally.
When to Use
• Large applications
• Testable systems
• Loosely coupled architecture
Example
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
public class OrderService
{
private readonly ILogger _logger;
public OrderService(ILogger logger)
{
_logger = logger;
}
public void ProcessOrder()
{
_logger.Log("Order processed");
}
}
Usage
ILogger logger = new ConsoleLogger();
var service = new OrderService(logger);
service.ProcessOrder();
Advantages of Dependency Injection
• Improves testability (easy mocking)
• Promotes loose coupling
• Centralized configuration of dependencies
Alternatives of Dependency Injection
• Service Locator (less transparent)
• Manual instantiation (tightly coupled)
2. Mediator Pattern
Purpose: Reduces direct communication between objects by introducing a mediator that handles interactions.
When to Use
• Complex object communication
• Reducing dependencies between components
Example
public interface IMediator
{
void Send(string message, Colleague colleague);
}
public abstract class Colleague
{
protected IMediator _mediator;
protected Colleague(IMediator mediator)
{
_mediator = mediator;
}
}
public class ConcreteMediator : IMediator
{
public Colleague1 Colleague1 { get; set; }
public Colleague2 Colleague2 { get; set; }
public void Send(string message, Colleague colleague)
{
if (colleague == Colleague1)
Colleague2.Receive(message);
else
Colleague1.Receive(message);
}
}
public class Colleague1 : Colleague
{
public Colleague1(IMediator mediator) : base(mediator) { }
public void Send(string message)
{
_mediator.Send(message, this);
}
public void Receive(string message)
{
Console.WriteLine("Colleague1 received: " + message);
}
}
public class Colleague2 : Colleague
{
public Colleague2(IMediator mediator) : base(mediator) { }
public void Send(string message)
{
_mediator.Send(message, this);
}
public void Receive(string message)
{
Console.WriteLine("Colleague2 received: " + message);
}
}
Usage
var mediator = new ConcreteMediator();
var c1 = new Colleague1(mediator);
var c2 = new Colleague2(mediator);
mediator.Colleague1 = c1;
mediator.Colleague2 = c2;
c1.Send("Hello from C1");
c2.Send("Hello from C2");
Advantages of Mediator Pattern
• Reduces coupling between classes
• Centralizes communication logic
• Easier to maintain complex systems
Alternatives of Mediator Pattern
• Observer Pattern (event-driven communication)
• Direct references (simpler but tightly coupled)
3. CQRS (Command Query Responsibility Segregation)
Purpose: Separates read operations (queries) from write operations (commands).
When to Use
• High-performance systems
• Complex domains
• Systems requiring scalability
Example
// Command
public class CreateOrderCommand
{
public string ProductName { get; set; }
}
// Query
public class GetOrdersQuery { }
// Command Handler
public class OrderCommandHandler
{
private readonly List _orders = new List();
public void Handle(CreateOrderCommand command)
{
_orders.Add(command.ProductName);
}
public List GetOrders()
{
return _orders;
}
}
Usage
var handler = new OrderCommandHandler();
handler.Handle(new CreateOrderCommand { ProductName = "Laptop" });
var orders = handler.GetOrders();
Advantages of CQRS
• Improves scalability
• Separates concerns clearly
• Optimizes read/write independently
Alternatives of CQRS
• CRUD-based architecture (simpler systems)
• Layered architecture without separation
4. Unit of Work Pattern
Purpose: Maintains a list of operations and commits them as a single transaction.
When to Use
• Database transactions
• Multiple related operations
Example
public class UnitOfWork
{
private readonly List _operations = new List();
public void Register(string operation)
{
_operations.Add(operation);
}
public void Commit()
{
foreach (var op in _operations)
{
Console.WriteLine($"Executing: {op}");
}
_operations.Clear();
}
}
Usage
var uow = new UnitOfWork();
uow.Register("Insert Order");
uow.Register("Update Inventory");
uow.Commit();
Advantages of Unit of Work
• Ensures transactional consistency
• Groups related operations
• Reduces database calls
Alternatives of Unit of Work
• Direct transaction handling
• ORM built-in transaction management
Summary Table
| Pattern | Purpose | Best Use Case | Alternative |
|---|---|---|---|
| Dependency Injection | Externalize dependencies | Testable architectures | Service Locator |
| Mediator | Centralized communication | Complex object interactions | Observer |
| CQRS | Separate read/write logic | High-scale systems | CRUD |
| Unit of Work | Transaction management | Database operations | ORM transactions |
Closing Thoughts
Advanced patterns are less about isolated techniques and more about how systems are structured as a whole. In real-world applications, these patterns are often combined:
• DI + Mediator (common in modern frameworks)
• CQRS + Unit of Work (data-heavy systems)
Understanding when not to use these patterns is just as important as knowing how to implement them. Overengineering can introduce unnecessary complexity, so always align pattern choice with actual requirements.