Skip to content

Application Design Patterns

A design pattern is a proven solution to a recurring problem in a particular context, and the context matters as much as the solution. System Design Patterns covers where the idea comes from; this page covers the equivalent within a single application’s codebase. These patterns differ from the Application Architecture Styles, which define how the whole application is organised.

In software, the seminal work is Design Patterns (1994) by Gamma, Helm, Johnson and Vlissides, the “Gang of Four”. As well as cataloguing 23 patterns, it put the principle “favour object composition over class inheritance” on the map. What followed was a wave of hype: articles, blog posts and conference talks reiterating the patterns, sometimes presenting them as an end in themselves. I’m sure there are projects where they were overused, but it hasn’t been much of an issue on the ones I’ve worked on. Over time the hype has faded, and the patterns have settled back into what they’re best at: a useful tool, and a common language for describing a design.

For enterprise applications, the more practical successor is Martin Fowler’s Patterns of Enterprise Application Architecture (2002), one of the books I value most. Many of the patterns I use day to day come from it, including Repository, Unit of Work, Data Mapper, and the choice between Transaction Script and Domain Model. Eric Evans’ Domain-Driven Design (2003) added the building blocks for modelling a complex domain.

The patterns I reach for most are the data access patterns and, where a module has real business complexity, the DDD building blocks. Many of the others I use through the framework rather than hand-rolling them. In .NET, the dependency injection container, the middleware pipeline, and Entity Framework’s DbContext (itself a Unit of Work) are all patterns built into the platform. The entries below cover the patterns I have a view on, with notes on what to watch out for.

Repository abstracts data access behind a collection-like interface, so the rest of the codebase can work with domain objects without knowing how they’re persisted. There’s a lot of commentary in the .NET community arguing that it isn’t needed, and advocating using the DbContext directly instead. The usual complaint is a real code smell: a repository that grows a long list of GetXxx methods, one for each screen or report. Every new query means another method and more mapping, and the LINQ operators that would make it flexible are hidden behind the abstraction.

I still think the Repository has value (see Key Concepts), but mainly on the write side: loading and saving aggregates through their aggregate root, which is also the write side of CQRS. Reads are a different job. Watch out for: leaking ORM-specific concerns through the abstraction, and a repository turning into a catalogue of query methods.

Query Object models each query, view, report or projection as its own class, outside the repository. It can use the DbContext directly, or build on a repository method that returns an IQueryable, so each query keeps the full flexibility of LINQ and can be shaped for whatever consumes it. A repository for aggregates on the write side, with query objects on the read side, is where I’d lean if I were starting a greenfield application today. Watch out for: an IQueryable escaping the query object, spreading query logic and ORM concerns into the calling code.

Unit of Work tracks changes made across one or more repositories during a business operation and commits them as a single transaction. In Entity Framework, the DbContext already plays this role.

Data Mapper vs. Active Record are two contrasting approaches to persistence. Data Mapper keeps the domain model ignorant of the database, translating between the two in a separate layer. Active Record has the object own its own persistence logic (e.g. save(), find()). Watch out for: Active Record coupling domain logic to the schema as the model grows.

These are the patterns I use where a module warrants a proper domain model, rather than a simpler transaction script (see Vertical-slice Architecture).

Entity is a domain object defined by a persistent identity rather than its attributes. Two entities with identical fields are still distinct if their identity differs.

Value Object is a domain object defined entirely by its attributes rather than an identity, and is immutable. Two value objects with the same attributes are interchangeable.

Aggregate is a cluster of entities and value objects treated as a single unit for consistency, with one entity (the aggregate root) as the only point of entry for changes. Watch out for: aggregates growing too large and becoming a bottleneck for concurrent changes.

Domain Event is a record of something meaningful that happened in the domain, used to notify other parts of the system without coupling the triggering code to what happens next.

Domain Service holds business logic that doesn’t naturally belong to a single entity or value object, typically because it coordinates across several of them. Watch out for: becoming a dumping ground for logic that should live on an entity instead.

Specification encapsulates a business rule as a composable, reusable object, so rules can be combined (and/or/not) instead of duplicated across the codebase. Watch out for: over-engineering simple conditionals into specification objects.

Dependency Injection supplies a component’s dependencies externally (from a caller or a container) rather than constructing them internally, making the component easier to test and reconfigure. Watch out for: over-abstracting simple dependencies, or a container that hides the actual object graph.

Factory encapsulates the logic for constructing an object, so callers don’t need to know the concrete type or construction steps involved. Watch out for: adding a factory where a plain constructor would do.

Adapter wraps an incompatible interface so it matches what the calling code expects. It’s the in-app counterpart to an Anti-Corruption Layer, translating at a class boundary rather than a system boundary.

Builder separates the construction of a complex object with many optional parts from its final representation, assembling it step by step. Especially useful for test fixtures.

Singleton restricts a class to exactly one instance with a global access point. Watch out for: global state that kills testability. Prefer a stateless service registered with the container as a single instance over a true singleton.

Middleware / Pipeline chains a series of request-processing steps, each able to act before and/or after passing control to the next. It’s commonly used for concerns like logging, authentication, validation and error handling. Watch out for: ordering-dependent middleware and unclear short-circuit behaviour.

Decorator adds behaviour to an object without changing its interface, by wrapping it in another object that implements the same contract. Watch out for: deep decorator chains becoming hard to trace.

Strategy swaps an algorithm’s implementation at runtime behind a common interface, letting the caller select behaviour without conditional branching.

Observer (in-process pub/sub) notifies registered dependents of state changes within a single application. Contrast with Event-Driven Architecture, which applies the same idea across services via a broker. Watch out for: implicit control flow that’s hard to trace when overused.

State changes an object’s behaviour based on its internal state, encapsulating each state’s behaviour separately instead of branching on a status flag throughout the codebase.

Command encapsulates a request as an object, enabling undo, queuing, or logging of operations that would otherwise be a direct method call.

Template Method defines the skeleton of an algorithm in a base class, letting subclasses fill in specific steps without changing the overall structure. Watch out for: inheritance-based extension, which is harder to test and reason about. This is where “favour composition over inheritance” applies most directly, so prefer composition where possible.

Exceptions mapped to Problem Details is the approach I’ve favoured to date. Failures are modelled as custom exception classes, and global handling in the middleware pipeline maps them to Problem Details responses, the standard format for errors in HTTP APIs. Handlers stay free of error-formatting code, and every API returns errors in the same shape. Watch out for: exceptions that aren’t mapped falling through as generic 500 responses, or leaking internal detail to the caller.

Result / Either is the main alternative: success or failure is represented explicitly in a function’s return type rather than via exceptions, making failure handling part of the type signature. It’s popular in functional languages and increasingly in .NET, but it isn’t the approach I’ve adopted. Watch out for: inconsistent adoption, mixing Result types with exceptions across a codebase.

  • Eric Evans — Domain-Driven Design: Tackling Complexity in the Heart of Software (2003).
  • Martin Fowler — Patterns of Enterprise Application Architecture (2002).
  • Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software (1994).
  • IETF — “RFC 9457: Problem Details for HTTP APIs” (2023).