Design Patterns Deep Dive · 4 of 4

Enterprise Patterns — The Ones You'll Actually Wire Into a Service

The Gang of Four catalog stops at the boundary of one process. Real services have to deal with persistence, transactions, transport, retries, distributed failures, and idempotent APIs. Most of these patterns come from Martin Fowler's Patterns of Enterprise Application Architecture (2002) and the distributed-systems literature that followed. They're the names that come up in every backend architecture review.

RepositoryUnit of WorkDTODIOutboxCircuit Breaker
← Back to Architecture
Quick Facts

What Counts as "Enterprise"

Basic Concepts

  • Persistence patterns: how the domain talks to a database without becoming the database.
  • Boundary patterns: how data crosses process or service edges.
  • Resilience patterns: how to keep working when dependencies fail.
  • Workflow patterns: how to coordinate multi-step operations across services.
  • Most are framework-supported. Spring, .NET, Django, Rails — all ship with first-class support for the persistence and DI patterns. The resilience and distributed patterns require explicit libraries (Resilience4j, Polly) and explicit thinking.
Persistence

Talking to the Database

Repository

Intent: Mediate between the domain and data-mapping layers using a collection-like interface for accessing domain objects.

userRepository.findById(id), userRepository.save(user). The repository hides whether data lives in Postgres, Mongo, a remote API, or memory. The domain depends on the abstraction, not the storage.

Why it matters: tests inject an in-memory repo and run in milliseconds. Swapping storage means writing one new repo, not changing the domain. It's the canonical example of dependency inversion in business code.

Don't: let the repository return ORM entities the domain has to know about, and don't add findUsersByEmailAndStatusAndCreatedBetween-style methods for every query — that's where Repositories turn into UserDaos in disguise.

Unit of Work

Intent: Track all the objects loaded and modified during a business transaction, then coordinate writing out changes and resolving concurrency at commit.

Built into nearly every ORM: Hibernate's session, EF Core's DbContext, SQLAlchemy's session. You change a few entities; on commit(), the UoW flushes them in dependency order in one transaction.

What it gives you: "save the order, save the customer, save the invoice — atomically" in one call. Optimistic concurrency checks, dirty tracking, change ordering — all UoW concerns.

Data Mapper vs Active Record

Active Record: the entity knows how to save itself. order.save(). Rails' ActiveRecord is the namesake; great for thin domains and rapid iteration. Domain logic and persistence are intertwined.

Data Mapper: a separate layer maps between domain objects and the DB. The domain doesn't know it's persisted. Hibernate, EF Core, JPA. More code, cleaner separation; the right call for richer domains.

Specification

Intent: Encapsulate a business rule as a small object that can be combined with other rules using and, or, not.

activeCustomers.and(inEurope). Cleaner than long boolean conditions sprinkled across services. Often used to build query criteria; some ORMs (EF Core, Spring Data) translate composed specifications into SQL WHERE clauses.

Boundaries

Data Crossing Edges

DTO — Data Transfer Object

Intent: A flat, serializable object whose only job is moving data across a process or service boundary.

DTOs keep your domain model from leaking into the wire format. Letting your User entity be your API response is convenient until you need to add a sensitive field, version the API, or shape the response differently per client. DTOs decouple the two concerns.

Mapping: hand-written, MapStruct (Java), AutoMapper (.NET), or just a constructor that takes the domain object. Don't over-engineer the mapping layer — the boundary code lives forever, but it doesn't need to be clever.

Dependency Injection & Inversion of Control

Intent: Don't construct your dependencies inside a class — receive them through the constructor, a setter, or a function parameter. The composition root wires concrete implementations together once, at startup.

DI is what makes Repository, Strategy, and Adapter actually pay off — none of them help if callers new their concrete dependencies anyway. Frameworks: Spring, .NET DI, NestJS, Guice, Dagger.

Constructor injection > setter injection > service locator. Constructor injection makes dependencies explicit and the object immutable; service locator hides them and recreates the global-state problems DI was meant to solve.

Anti-Corruption Layer

Intent: Insulate your domain from another model's vocabulary, especially when integrating with a legacy or third-party system whose concepts don't match yours.

A translator sits at the boundary, converting their model into yours and vice versa. Critical when integrating with a system whose design choices you don't want to inherit — the ACL is what stops their concepts from leaking everywhere.

Resilience

When Things Fail (and They Will)

Circuit Breaker

Intent: Wrap calls to a remote service. After N consecutive failures, "open" the circuit and fail fast for a cooldown window — don't keep hammering a sick dependency.

After the timeout, the breaker goes half-open, lets one trial request through, and either closes (success) or re-opens (failure). Built into Resilience4j (Java), Polly (.NET), Hystrix (deprecated but historically important), Istio, Envoy.

Why it matters: without a breaker, a slow dependency drains your thread/connection pool until the whole service falls over. The breaker turns "everything is timing out" into "we know that one thing is sick; everything else is fine."

Bulkhead

Intent: Isolate resources per dependency so one failing collaborator can't drain shared pools.

Separate thread pools, connection pools, or semaphore counts per downstream. The recommendations service can saturate its pool without starving the checkout service of threads. Named for ship bulkheads — flooding one compartment doesn't sink the whole ship.

Retry, Timeout, Backoff

Timeout: every cross-service call has one. Always. The default in most HTTP clients is "infinite" — don't accept it.

Retry: with exponential backoff and jitter. Only on idempotent operations — retrying a non-idempotent POST may double-charge a card. Always cap the total retry budget.

The combination matters. Retry without backoff turns one outage into a self-inflicted DoS. Retry without idempotency turns transient errors into duplicated business actions.

Idempotency Key

Intent: The caller sends a unique key with each mutating request; the server records it and short-circuits duplicates with the original response.

Lets clients retry safely over flaky networks. Stripe, payment APIs, and most modern POST endpoints support this — the key lives in a header (Idempotency-Key), the server stores it for some retention window.

Distributed Workflows

Patterns Across Services

Outbox Pattern

Intent: Reliably publish events from a service that owns a relational DB, without dual-write inconsistency.

Inside the same DB transaction as the state change, write the event to an outbox table. A separate process polls the outbox (or uses CDC like Debezium) and publishes to the broker. No "wrote to DB but failed to publish" gap. Essential whenever an event has to go out as a consequence of a DB write.

Saga

Intent: Implement a long-running business transaction across services where ACID isn't possible. Each step has a compensating action that undoes it.

Place order → reserve inventory → charge card → notify warehouse. If the charge fails, fire compensating actions backward: release the reservation, mark the order canceled. Implement choreographed (events) or orchestrated (workflow engine like Temporal, Step Functions).

API Gateway / BFF

Intent: Provide a single entry point for clients in front of many backend services.

Handles auth, rate limiting, request shaping, response composition. The Backend-for-Frontend variant goes further: one gateway per client type (web, iOS, Android), each composing the underlying services into the exact shape that client needs. Stops backend services from getting cluttered with client-specific endpoints.

Strangler Fig

Intent: Incrementally replace a legacy system by routing more and more traffic to the new one, until the old one can be removed.

Named after the strangler fig vine that grows around a host tree until the host dies. Used for monolith-to-microservices migrations: a routing layer sends traffic for a slice of functionality to the new service while everything else still hits the old monolith. Repeat until the monolith is empty.

Sidecar

Intent: Deploy a helper process alongside the main service to handle cross-cutting concerns — TLS, observability, config, service discovery.

The basis of service meshes (Istio, Linkerd). Your application stays focused on business logic; the sidecar handles mTLS, traffic shaping, retries, telemetry. Ambient meshes (Istio Ambient, Linkerd) move some of this off-pod, but the pattern is the same.

Continue

Other Pattern Families