Microservices Architecture & Design

Patterns & Anti-Patterns

18 min Lesson 9 of 12

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:

# application.yml — Spring Cloud Gateway strangler configuration spring: cloud: gateway: routes: - id: orders-service uri: lb://orders-service # route to new microservice predicates: - Path=/api/orders/** - id: legacy-monolith uri: http://legacy.internal:8080 # everything else still goes to the monolith predicates: - Path=/**

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.

Sidecar vs. library: A shared library forces every team to upgrade together and ties the runtime language. A sidecar is language-agnostic, upgradeable independently, and keeps service code focused on business logic. This is why service meshes (Istio, Linkerd) are built on the sidecar model.

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:

// pom.xml dependency // <artifactId>resilience4j-spring-boot3</artifactId> @Service public class InventoryClient { @CircuitBreaker(name = "inventoryService", fallbackMethod = "defaultInventory") public InventoryResponse checkStock(String productId) { return restClient.get() .uri("/inventory/{id}", productId) .retrieve() .body(InventoryResponse.class); } private InventoryResponse defaultInventory(String productId, Throwable t) { // return a safe degraded response rather than propagating the failure return InventoryResponse.unavailable(productId); } }
# application.yml — circuit breaker thresholds resilience4j: circuitbreaker: instances: inventoryService: sliding-window-size: 10 failure-rate-threshold: 50 # open after 50 % of last 10 calls fail wait-duration-in-open-state: 10s permitted-number-of-calls-in-half-open-state: 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.

Choreography vs. orchestration: In a choreographed saga each service reacts to events autonomously — low coupling, but hard to reason about. In an orchestrated saga a central saga orchestrator (e.g., a Spring state machine or Axon Framework) explicitly commands each participant — easier to trace, but a potential single point of control. Prefer orchestration when the saga has more than three steps or when an audit trail is a regulatory requirement.

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.

// ACL: translate legacy CRM contact into your own Customer model @Component public class CrmContactTranslator { public Customer fromLegacy(LegacyCrmContact contact) { return new Customer( new CustomerId(contact.getContactId()), new FullName(contact.getFirstName(), contact.getLastName()), Email.of(contact.getEmailAddress()) ); } public LegacyCrmContact toLegacy(Customer customer) { LegacyCrmContact contact = new LegacyCrmContact(); contact.setContactId(customer.getId().value()); contact.setFirstName(customer.getName().first()); contact.setLastName(customer.getName().last()); contact.setEmailAddress(customer.getEmail().value()); return contact; } }

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 dto or model library is imported by every service. Changing a field requires releasing and redeploying all consumers simultaneously.
Shared libraries are a coupling time-bomb. A 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.