Patterns & Anti-Patterns
Patterns & Anti-Patterns
Microservices architecture has matured enough that a clear vocabulary of patterns — proven solutions to recurring problems — and anti-patterns — approaches that look reasonable but cause serious harm at scale — has emerged from the collective experience of thousands of production systems. Knowing this vocabulary lets you recognise a problem early, name it precisely, and apply the right remedy.
The Strangler Fig Pattern
When migrating a legacy monolith to microservices, you rarely have the luxury of stopping the world and rewriting everything. The Strangler Fig pattern (named after the tree that grows around its host) lets you extract functionality incrementally. A routing layer in front of the monolith forwards specific routes to new microservices as they are ready; the monolith shrinks over time until it can be decommissioned entirely.
In Spring Cloud Gateway, the routing layer is trivially expressed in YAML:
Each team can extract one bounded context at a time without a risky big-bang cutover. Security does not regress either: the gateway enforces authentication before routing, so both the monolith and the new service are protected by the same token validation.
The Sidecar Pattern
Cross-cutting concerns such as mTLS, distributed tracing, and retries should not live in every service's application code. The Sidecar deploys a helper container beside each service instance — in the same pod in Kubernetes — that transparently handles those concerns via intercepted network traffic.
The Circuit Breaker Pattern
You have seen circuit breakers discussed in the context of resilience, but they are also an architectural pattern worth naming clearly here. A service that calls a downstream dependency should wrap that call in a circuit breaker: a state machine that tracks failure rates and, when the failure threshold is crossed, opens the circuit so that subsequent calls fail fast without even reaching the dependency.
With Resilience4j in Spring Boot 3:
The Saga Pattern (Revisited as a Pattern)
Distributed transactions across microservices are impossible with a single ACID commit. The Saga pattern decomposes a long-running business transaction into a sequence of local transactions, each publishing an event or command that triggers the next step. If any step fails, compensating transactions undo the preceding work.
The Anti-Corruption Layer
When integrating with a legacy system or a third-party service that uses a different domain model, do not let that external model leak into your own bounded context. Introduce an Anti-Corruption Layer (ACL) — a translation boundary that converts incoming data into your own model and outgoing data into theirs.
The ACL ensures that a breaking change in the legacy system's model requires a change only in the translator, not throughout your entire service.
The Distributed Monolith Anti-Pattern
The distributed monolith is the most dangerous anti-pattern in microservices. It looks like a microservices deployment — multiple services, separate repos, independent deploys — but it behaves like a monolith because the services are tightly coupled at runtime.
Symptoms of a distributed monolith:
- Shared database: Multiple services read and write the same tables. A schema change requires co-ordinating multiple teams.
- Synchronous chains: Service A calls B which calls C which calls D in a single request. Any node going down takes the entire chain with it.
- Chatty interfaces: Ten fine-grained REST calls for data that could be co-located in one service, adding latency and failure surface on every request.
- Shared domain objects: A common
dtoormodellibrary is imported by every service. Changing a field requires releasing and redeploying all consumers simultaneously.
common-models JAR that carries domain objects across service boundaries is the most common way a microservices project silently becomes a distributed monolith. Accept some duplication — a UserSummary DTO in each service — rather than sharing a User entity across the network boundary.
The root cause of the distributed monolith is almost always incorrect service boundary design — services were split along technical layers (UI service, business-logic service, data service) rather than along domain capabilities (order management, shipping, billing). Revisit Lesson 3 on Bounded Contexts whenever you find yourself fighting synchronous coupling.
The Chatty API Anti-Pattern
Calling a remote service has a latency floor that in-process calls do not. An API that requires clients to make ten calls to assemble one page worth of data magnifies that floor by ten. The remedy is one of:
- Coarse-grained aggregation: Add an endpoint that returns all the data a consumer needs in one call.
- GraphQL or BFF (Backend for Frontend): A dedicated BFF service (often one per client type — web, mobile) assembles data from multiple services server-side and returns it in a single payload, eliminating client-side fan-out.
- Domain event projection: Project the data a consumer needs into its own read model (CQRS read side) so it can answer queries locally.
Naming Your Own Service Decomposition
A practical heuristic: if two services are almost always deployed together, they are probably one service. Similarly, if changing one always forces a change in the other, they share a business concept that belongs in a single bounded context. Use this lens during code review and architectural fitness function checks to catch coupling drift before it calculates into a distributed monolith.
Summary
The patterns covered here — Strangler Fig, Sidecar, Circuit Breaker, Saga, and Anti-Corruption Layer — each solve a specific class of problem. The distributed monolith and chatty API anti-patterns are the failure modes most teams hit first. The final lesson of this tutorial puts all of these ideas together in a practical design exercise.