Design Patterns I Keep Coming Back To
October 10, 2025 · 6 min read
Design Patterns, .NET, Architecture, Software Engineering
After years of building enterprise systems, I've realized that not all design patterns are created equal. Some I use daily without thinking about them. Others I learned once for an interview and never touched again. Here's my honest breakdown of what actually matters.
The Patterns I Actually Use Daily
Creational Patterns
Singleton - I rarely implement this myself anymore. DI containers handle it with AddSingleton(). But understanding thread-safety issues here has saved me from some nasty production bugs.
Factory Method - This one's everywhere. IHttpClientFactory, ILoggerFactory - I use these without even thinking about it being a "pattern." The key insight: create objects without specifying the exact class.
Builder - Fluent APIs have made this pattern second nature:
var app = WebApplication.CreateBuilder(args)
.ConfigureServices()
.ConfigureLogging()
.Build();
Every time I chain methods like this, that's the Builder pattern at work.
Structural Patterns
Decorator - ASP.NET Core middleware is essentially a decorator chain. Understanding this helped me write better custom middleware:
app.UseAuthentication(); // Decorator 1
app.UseAuthorization(); // Decorator 2
app.UseCustomMiddleware(); // My decorator
Adapter - I reach for this constantly when wrapping third-party APIs or integrating legacy systems. The goal: make incompatible interfaces work together.
Facade - My service layers are often facades. They hide the complexity of multiple repositories, external APIs, and business logic behind a clean interface.
Behavioral Patterns
Strategy - Swapping algorithms at runtime. Payment processors, validation strategies, different export formats. Once you see it, you can't unsee how useful this is:
public interface IPaymentStrategy
{
Task<PaymentResult> ProcessAsync(Order order);
}
// Swap implementations without changing calling code
Chain of Responsibility - The entire ASP.NET middleware pipeline. Also useful for validation pipelines and approval workflows.
Command - MediatR made me appreciate this. Encapsulating requests as objects enables so much: logging, validation, undo/redo, queuing.
Architecture Patterns That Changed How I Think
CQRS
This was a game-changer for me. Separating read and write models sounds simple, but the implications are huge:
Commands (writes) → Domain logic → SQL Server (source of truth)
↓ Events
Queries (reads) → Optimized read model → Cosmos/Redis (fast reads)
I don't use full CQRS everywhere, but even partial separation (different DTOs for reads vs writes) has cleaned up so much code.
Event Sourcing
I only use this where audit trails matter - financial systems, compliance-heavy domains. The idea of storing events instead of current state felt weird at first, but it's powerful when you need to answer "what happened and when?"
Repository Pattern - My Honest Take
Controversial opinion: with EF Core, I often skip the repository abstraction. DbContext already implements Unit of Work. Adding another layer sometimes just adds complexity without benefit.
That said, for complex queries or when I need to mock data access in tests, repositories still make sense.
Cloud Patterns I Learned the Hard Way
Working with Azure taught me these through painful production incidents.
Retry with Exponential Backoff
Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, attempt =>
TimeSpan.FromSeconds(Math.Pow(2, attempt)));
The first time a downstream service had a brief hiccup and my entire application fell over, I learned why this matters.
Circuit Breaker
Stop hammering a failing service. Give it time to recover:
Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromMinutes(1));
Saga Pattern
Distributed transactions across microservices. When the payment service succeeds but inventory fails, you need compensating actions:
Order → Payment → Inventory
↑ ↑ ↓
└── Compensate ←────┘ (on failure)
Outbox Pattern
This one took me a while to appreciate. The problem: you save to the database and publish a message, but what if the message publish fails after the DB commit?
Solution: save the message to an outbox table in the same transaction, then publish from there.
Strangler Fig
For migrating legacy systems. Route traffic gradually to the new implementation. I used this to migrate a monolith to microservices over 18 months without a "big bang" release.
DDD Patterns - When Complexity Demands It
I only reach for full DDD on complex domains. For CRUD apps, it's overkill.
| Pattern | When I Use It |
|---|---|
| Aggregate | When multiple entities must change together consistently |
| Value Object | Immutable concepts like Money, Address, DateRange |
| Domain Event | When something happens that other parts of the system care about |
| Bounded Context | Large systems with distinct subdomains |
| Anti-Corruption Layer | Integrating with messy external systems |
Modern .NET Patterns I've Adopted
Vertical Slice Architecture
Organizing by feature instead of layer has been refreshing:
Features/
├── CreateOrder/
│ ├── CreateOrderCommand.cs
│ ├── CreateOrderHandler.cs
│ └── CreateOrderEndpoint.cs
├── GetOrder/
│ ├── GetOrderQuery.cs
│ └── GetOrderHandler.cs
Everything for a feature lives together. No more jumping between Controllers/, Services/, Repositories/ folders.
Result Pattern
Railway-oriented programming for cleaner error handling:
return await GetCustomer(id)
.Bind(customer => ValidateOrder(customer, order))
.Bind(order => ProcessPayment(order))
.Map(payment => new OrderConfirmation(payment));
No more scattered try-catch blocks. Errors flow through the pipeline.
Minimal APIs
For simple endpoints, the ceremony of controllers feels heavy:
app.MapPost("/orders", async (CreateOrderCommand cmd, ISender sender) =>
await sender.Send(cmd));
My Quick Reference
When I need to pick a pattern:
| Scenario | My Go-To Patterns |
|---|---|
| Flexible object creation | Factory, Builder |
| Adding behavior without changing class | Decorator |
| Multiple interchangeable algorithms | Strategy |
| Processing through multiple handlers | Chain of Responsibility |
| Decoupling components | Observer, Mediator |
| Distributed transactions | Saga, Outbox |
| Resilient HTTP calls | Retry, Circuit Breaker |
| Complex domain logic | DDD patterns |
| Separating read/write concerns | CQRS |
| Migrating legacy systems | Strangler Fig |
What Interviewers Actually Ask
In my experience, these come up most frequently:
- Singleton - Thread safety, lazy initialization, why DI is better
- Factory - When and why, real examples like IHttpClientFactory
- Strategy - Swapping algorithms, payment processor example
- Repository - And whether it's needed with EF Core (trick question!)
- CQRS - Especially with MediatR
- Circuit Breaker / Retry - Polly, resilience patterns
- Dependency Injection - Not GoF but absolutely essential
The patterns I've outlined here aren't just theoretical knowledge - they're tools I've used to solve real problems. The key is knowing when to apply them and, equally important, when not to over-engineer with patterns you don't need.