Design Patterns (GoF + Enterprise) in C#
In short: Reusable, named solutions to recurring design problems.
- GoF (1994) lists 23 in three categories: Creational (Singleton, Factory Method, Abstract Factory, Builder, Prototype), Structural (Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy), Behavioural (Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor).
In short: Use Lazy<T>: private static readonly Lazy<Svc> _i = new(() => new Svc()); public static Svc Instance => _i.Value; with a private constructor and a sealed class.
Lazy<T>is thread-safe by default (ExecutionAndPublication).- Alternatives: a
static readonlyfield (thread-safe because of the type initialiser), or double-checked locking withvolatile. - In ASP.NET Core, prefer
services.AddSingleton<ISvc, Svc>(). - It's testable and the container manages the lifetime.
In short: Factory Method creates one product, and subclasses (or a method) decide which concrete class to create.
- Abstract Factory creates a family of related products through an interface with several create methods (button + textbox for a theme;
- connection + command + parameter for a DB provider).
- Factory Method uses inheritance; Abstract Factory uses composition (you pass the factory object).
In short: It encapsulates interchangeable algorithms behind an interface and selects one at runtime.
- Examples: discount rules, shipping cost calculators, payment gateways, export formats, tax rules by country.
- With DI, register every strategy and inject
IEnumerable<IStrategy>, or use keyed services (.NET 8). - It removes
switchstatements and satisfies OCP.
In short: A Decorator keeps the same interface and adds behaviour (caching, logging, retry) by wrapping an object.
- You can stack several.
- An Adapter changes the interface so an incompatible class fits what the client expects (wrapping a vendor SDK behind
ISmsSender). - In short, Decorator = same interface + extra behaviour; Adapter = different interface, same behaviour.
In short: A Repository hides data access behind a collection-like interface; Unit of Work groups changes into one transaction.
- EF Core already implements both (
DbSet<T>,DbContext.SaveChanges). - Adding them on top is optional.
- Pros: domain code doesn't reference EF, queries are centralised, testing is easier.
- Cons: extra layers, and generic repositories often leak
IQueryableor limit EF features. - A balanced answer: aggregate-specific repositories in DDD/Clean Architecture;
- direct
DbContextusage in simpler apps, with integration tests using SQLite/Testcontainers.
In short: Each middleware receives the HttpContext and a next delegate.
- It can act before and after calling
next, or short-circuit (e.g. return 401 without calling the rest). - The order of
app.Use...calls defines the chain: exception handler → HTTPS redirection → static files → routing → CORS → authentication → authorization → endpoints.
In short: Observers subscribe to a subject and get notified of changes.
- C# supports it with events and delegates (
event EventHandler<T>),IObservable<T>/IObserver<T>(with Rx.NET),INotifyPropertyChangedin UI binding, and at architecture level pub/sub messaging (Azure Service Bus topics, Kafka). - Pitfall: forgetting to unsubscribe causes memory leaks.
In short: A Facade provides a simple API over a complicated subsystem.
- Example: a
CheckoutService.PlaceOrder()that coordinates inventory, payment, shipping and email so the controller makes one call. - In .NET,
File.ReadAllTextis a facade overFileStream+StreamReader+ encoding detection.
Builder constructs complex objects step by step, often fluently, and validates at Build(). .NET examples: WebApplication.CreateBuilder(args) → builder.Build(), StringBuilder, UriBuilder, SqlConnectionStringBuilder, HostBuilder, EF Core's ModelBuilder fluent API, and test-data builders in unit tests.
In short: Mediator centralises communication so components don't reference each other directly.
- MediatR implements an in-process mediator: controllers
Senda request object, and exactly one handler processes it. - Notifications can go to many handlers.
- Pipeline behaviours add cross-cutting concerns (validation, logging, transactions).
- Note: MediatR moved to a commercial licence from version 13 (2025).
- Some teams write a small mediator themselves or call handlers directly.
In short: Both vary part of an algorithm.
- Template Method uses inheritance: the base class fixes the skeleton and subclasses override steps, decided at compile time.
- Strategy uses composition: the whole algorithm is an injected object, swappable at runtime.
- Prefer Strategy for flexibility; Template Method suits frameworks with a fixed workflow (e.g. ETL importers,
BackgroundService.ExecuteAsync).
In short: Command Query Responsibility Segregation splits writes (commands, rich domain model, validation) from reads (queries, flat DTOs, maybe Dapper/views/a separate read DB).
- It's worth it when read and write workloads or shapes differ a lot, for complex domains, or for high read scale.
- It can be as light as separate command and query handler classes on one DB.
- It adds complexity, and with separate stores you get eventual consistency.
- Don't use it for simple CRUD.
In short: It's for publishing an integration event reliably when a DB change commits.
- Writing to the DB and then publishing to a broker is not atomic (dual-write problem).
- With Outbox, the event row is inserted into an
OutboxMessagestable in the same transaction as the business change. - A background processor reads unsent rows, publishes them, and marks them sent.
- Delivery is at-least-once, so consumers must be idempotent (Inbox table or dedupe key).
- Libraries: MassTransit, NServiceBus, Wolverine, CAP.
In short: Retry handles brief transient faults (timeouts, 503, 429) with exponential backoff and jitter.
- The Circuit Breaker wraps retries: if failures exceed a threshold, it opens and fails fast for a break period, protecting the struggling dependency and your own threads.
- Then it goes half-open to probe, and closes on success.
- In .NET 8+,
AddStandardResilienceHandler()configures them with timeouts and rate limiting. - Retry only idempotent operations, or use idempotency keys.
In short: It coordinates a distributed transaction across microservices without 2PC: a sequence of local transactions, each publishing an event or command.
- If a step fails, compensating transactions undo earlier steps (refund the payment, release the stock).
- Orchestration: a central saga orchestrator (state machine) tells services what to do.
- Choreography: services react to each other's events.
- Orchestration is easier to follow; choreography is more decoupled.
In short: It encapsulates a business rule or query criterion in an object (IsActiveCustomerSpec, OrdersAboveAmountSpec(1000)).
- You can combine them (And/Or/Not) and reuse them for in-memory checks and EF Core queries (as
Expression<Func<T,bool>>). - It keeps query logic out of controllers and avoids duplicated
Whereclauses. - Ardalis.Specification is a popular library.
In short: A Decorator: CachedProductRepository : IProductRepository wraps the existing repository, checks IMemoryCache/IDistributedCache (Redis) first, and falls back to the inner repository on a miss.
- Register it with Scrutor's
Decorate<,>or manual factory registration. - No existing code changes (OCP).
- Add expiration, and invalidate on product updates (remove the cache key in the update path, or publish an event).
In short: Define ISmsSender.
- Write an Adapter per vendor SDK (
TwilioSender,GupshupSender,MsgKitSender), registered as keyed services orIEnumerable<ISmsSender>. - Choose the active one from
IOptionsMonitor<SmsOptions>(Strategy) so config changes apply without a restart. - Add a failover decorator that tries the primary and, on failure or an open circuit (Polly Circuit Breaker), uses the secondary.
- Log the vendor used, and make sends idempotent with a message ID.
In short: Chain of Responsibility: IApprover with SetNext(IApprover) and Handle(Expense).
- Each approver handles it if the amount is within its limit, otherwise it passes it on.
- Build the chain from configuration so limits and new levels don't need code changes.
- A State pattern can track the expense's lifecycle (Submitted → Approved/Rejected → Paid).
OrderPlaced to Service Bus, and never lose the message even if the broker is down. How?In short: Use the Transactional Outbox: in the same SaveChanges transaction, insert the order and an outbox row with the serialized event.
- A
BackgroundServicepolls unsent outbox rows, publishes them to Service Bus, and marks them sent, retrying while the broker is down. - Consumers dedupe by message ID (idempotent consumer / Inbox).
- MassTransit's EF Core outbox does this out of the box.