Skip to content

System Design Patterns

A design pattern is a proven solution to a recurring problem in a particular context. The idea comes from the architect Christopher Alexander, whose A Pattern Language (1977) describes each pattern as a problem “which occurs over and over again in our environment”, together with “the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.” The context matters as much as the solution. A pattern that fits one problem is overhead in another, which is why the entries below include notes on what to watch out for (see Context Is King).

The patterns on this page apply within or across a system: how services communicate, stay consistent and cope with failure. They differ from the System Architecture Styles, which define how a whole system is organised. See Application Design Patterns for the equivalent within a single codebase.

The Gang of Four and Fowler’s Patterns of Enterprise Application Architecture dealt with code inside one application. At the system level, the counterpart is Gregor Hohpe and Bobby Woolf’s Enterprise Integration Patterns (2003), the pattern language of messaging and integration from the BizTalk and ESB era. It gave names to many of the messaging patterns still in use today, including Competing Consumers, Dead Letter Channel and Claim Check. More recently, Chris Richardson’s microservices.io catalogued the patterns that came with microservices, such as Saga, Transactional Outbox and API Gateway, and Microsoft’s Cloud Design Patterns covers many of the same ideas in cloud terms.

The patterns I reach for most are the messaging basics (Competing Consumers and Dead Letter Queue), the Outbox for reliable publishing, and API Gateway or Backend-for-Frontend at the edge. The resilience patterns matter just as much. In a distributed system the network won’t always behave, so they should be considered for any call that crosses a service boundary, rather than added as an afterthought.

Strangler Fig incrementally replaces a legacy system by routing traffic through a facade that gradually redirects functionality to new services, until the legacy system can be retired. Watch out for: routing complexity during the transition period.

API Gateway provides a single entry point for multiple backend services, handling cross-cutting concerns like routing, authentication and rate limiting. Watch out for: becoming a god object, and a single point of failure with a large blast radius.

Backend-for-Frontend (BFF) gives each frontend (web, mobile, etc.) its own dedicated backend, tailored to that client’s specific needs rather than sharing one general-purpose gateway. Watch out for: logic duplicated across backends, and the extra services to build and run.

Anti-Corruption Layer isolates a bounded context from a legacy or external system’s model, translating between the two so the external system’s complexity or inconsistencies don’t leak in. It has standalone value for isolating complexity, not just as a migration aid.

Circuit Breaker stops calling a failing downstream dependency after repeated failures, giving it time to recover and preventing cascading slowdowns. Watch out for: misconfigured thresholds causing premature tripping.

Retry with Exponential Backoff + Jitter automatically retries transient failures on idempotent calls, spacing retries out to avoid overwhelming a recovering dependency. Watch out for: retry storms, and applying it to non-idempotent operations.

Bulkhead isolates failure domains (for example, separate thread or connection pools per dependency) so a failure in one part of the system doesn’t exhaust shared resources and cascade elsewhere. Watch out for: correctly sizing each pool and monitoring them individually.

Timeout bounds how long any external call is allowed to take, so a slow dependency can’t stall the caller indefinitely. Watch out for: timeouts that don’t fit together across a call chain, such as an inner call allowed longer than its caller will wait.

Saga manages a transaction that spans multiple services as a sequence of local transactions, each with a compensating action to undo it if a later step fails. A saga can be orchestrated by a central coordinator, or choreographed, with each service reacting to the events of the others. Watch out for: the complexity of designing correct compensating transactions.

My background here is BizTalk, which explicitly models orchestrations and favours that side. It was also very heavyweight: resource-intensive, complex, and hard to manage and version. Having seen the pain of central orchestration, I lean towards choreography, with a lightweight orchestrator where a process genuinely needs one owner. Choreography has its own cost in observability, because the overall process is never written down in one place, so I’d pair it with distributed tracing and correlation IDs from the start.

Outbox Pattern guarantees that a database write and an event publish happen atomically, by writing the event to an outbox table in the same transaction and relaying it separately. Watch out for: the added complexity of the polling or CDC relay mechanism.

Sharding partitions data across multiple nodes so no single node needs to hold or serve all of it. Watch out for: cross-shard queries and the cost of rebalancing.

Cache-Aside has the application check the cache first and, on a miss, load from the primary store and populate the cache, reducing read load on that store. Watch out for: cache invalidation, and a thundering herd on a cold cache.

Competing Consumers has multiple consumer instances read from the same queue to parallelise processing. Watch out for: message ordering requirements, which this pattern breaks.

Dead Letter Queue isolates messages a consumer can’t process, routing them aside instead of blocking or endlessly retrying them. Watch out for: a DLQ backlog going unmonitored.

The next two patterns belong together. Both are answers to the same modelling question: how much data should a business event carry? At one end, a thin notification says only that something happened, and consumers call back to the source for the details. The two patterns below sit further along that spectrum.

Event-Carried State Transfer has events carry enough state for consumers to act without calling back to the source, trading payload size for decoupling. Watch out for: large event payloads and schema coupling between producer and consumers.

Claim Check passes large payloads through a message broker by publishing a reference to externally stored data instead of the data itself. Watch out for: the added storage hop and latency.

Sidecar / Service Mesh offloads cross-cutting concerns such as routing, observability and security from individual services into a co-located proxy, keeping that logic out of application code. It’s the mechanism underneath tools like Dapr and many Kubernetes (AKS) platform features, so it’s worth understanding even when you don’t build on it directly.

  • Christopher Alexander et al. — A Pattern Language: Towns, Buildings, Construction (1977).
  • Gregor Hohpe and Bobby Woolf — Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions (2003).
  • Microsoft — “Cloud Design Patterns”, Azure Architecture Center.
  • Chris Richardson — “A pattern language for microservices”, microservices.io.