Microservices Design Patterns Every Backend Developer Should Know
From API gateways and circuit breakers to sagas and CQRS—practical microservices patterns, when to use them, and anti-patterns to avoid.
Microservices promise independent deployment, team autonomy, and technology flexibility. They also introduce network latency, distributed failures, and data consistency puzzles that monoliths hide behind a single process boundary. Design patterns exist because teams kept solving the same problems—and making the same mistakes.
This guide covers the patterns you will encounter (or need) when building or maintaining a microservices architecture, with enough practical context to know when each one applies.
When microservices are worth the complexity
Not every application needs microservices. A modular monolith with clear internal boundaries often ships faster and operates more simply.
Microservices make sense when:
- Multiple teams deploy independently on different schedules
- Parts of the system have genuinely different scaling profiles (a read-heavy catalog vs a write-heavy order pipeline)
- Regulatory or security boundaries require hard isolation
- You need polyglot persistence (Postgres for transactions, Elasticsearch for search, Redis for sessions)
If none of these apply, a well-structured monolith with good module boundaries is usually the better starting point.
API Gateway
An API Gateway sits between clients and your internal services. It handles routing, authentication, rate limiting, SSL termination, and request aggregation.
Client → API Gateway → [Orders Service, Users Service, Catalog Service]
Why it matters: Clients should not need to know your internal topology. When you split or merge services, the gateway absorbs routing changes.
Popular implementations: Kong, AWS API Gateway, NGINX, Envoy with custom config, or framework-specific gateways like Spring Cloud Gateway.
Watch out for: Turning the gateway into a "god service" with business logic. Keep it thin—routing, auth, and cross-cutting concerns only.
Backend for Frontend (BFF)
A BFF is a dedicated backend tailored to a specific client type (web, mobile, admin dashboard). Instead of one generic API serving all clients, each client gets an optimized aggregation layer.
A mobile BFF might return smaller payloads and fewer round trips. A web BFF might include richer data for a desktop layout.
BFFs prevent the lowest-common-denominator API problem where every endpoint tries to satisfy every client and ends up bloated.
Service Discovery
In a dynamic environment where instances come and go, hard-coded hostnames fail. Service discovery lets services find each other by logical name.
| Approach | Examples |
|---|---|
| Client-side discovery | Eureka, Consul — client queries registry, picks instance |
| Server-side discovery | Kubernetes DNS, AWS ALB — platform routes to healthy instance |
| Service mesh | Istio, Linkerd — sidecar proxies handle discovery and routing |
On Kubernetes, service discovery is largely solved: deploy a Service, other pods reach it by DNS name. Off Kubernetes, you need an explicit registry or platform feature.
Circuit Breaker
When Service A calls Service B and B is slow or down, A's threads (or connections) pile up waiting. Eventually A fails too—a cascade failure.
A circuit breaker monitors call failures. After a threshold, it "opens" and fails fast without waiting. After a cooldown, it allows a trial call to see if B recovered.
States:
- Closed — Normal operation, requests pass through
- Open — Fail immediately, return fallback or error
- Half-open — Allow one test request to check recovery
Libraries: Resilience4j (Java), Polly (.NET), opossum (Node.js). Implementations often pair with timeouts and bulkheads.
Retry with Exponential Backoff
Transient failures (network blips, brief overload) often succeed on retry. Blind retries amplify load during outages—the classic retry storm.
Best practices:
- Retry only on idempotent operations or with idempotency keys
- Use exponential backoff with jitter
- Cap maximum retry attempts
- Combine with circuit breakers
Attempt 1 → wait 100ms → Attempt 2 → wait 200ms → Attempt 3 → fail
Saga Pattern
Distributed transactions across services cannot use a single database COMMIT. The Saga pattern breaks a business transaction into a sequence of local transactions, each with a compensating action if a later step fails.
Choreography saga: Services listen for events and react. Order service publishes OrderCreated; payment service charges; inventory service reserves stock. If inventory fails, payment service listens for OrderFailed and refunds.
Orchestration saga: A central coordinator tells each service what to do and handles compensation logic.
| Style | Pros | Cons |
|---|---|---|
| Choreography | Loose coupling, no coordinator | Hard to trace; implicit flow |
| Orchestration | Clear flow, easier debugging | Coordinator is a dependency |
Use sagas for order processing, booking systems, and any workflow spanning multiple services with consistency requirements.
CQRS (Command Query Responsibility Segregation)
CQRS separates read models from write models. Commands change state through one path; queries read from optimized stores, which may be denormalized projections.
This pattern shines when read and write patterns differ dramatically—an e-commerce catalog with millions of reads and infrequent inventory updates, for example.
CQRS adds complexity. Do not adopt it because it sounds architectural; adopt it when measured read/write asymmetry justifies separate models.
Event Sourcing
Event sourcing stores state changes as a sequence of events rather than overwriting current state. The current state is derived by replaying events.
Pairs naturally with CQRS: the event log is the write model; projections serve queries.
Benefits: complete audit trail, temporal queries ("what did the account look like on March 1?"), and natural integration with event-driven architectures.
Costs: event schema evolution, snapshot management, and operational complexity. Most teams use event sourcing for specific domains (financial ledger, order history), not entire systems.
Strangler Fig
Migrating a monolith to microservices all at once is risky. The Strangler Fig pattern incrementally replaces functionality:
- Put a facade (gateway) in front of the monolith
- Build a new service for one bounded context
- Route that context's traffic to the new service
- Repeat until the monolith is "strangled"
This is the most practical migration pattern for legacy systems. Each slice delivers value without a big-bang rewrite.
Database per Service
Each microservice owns its data store. No shared database tables across service boundaries.
Why: Independent schema evolution, appropriate database choice per domain, clear ownership.
Challenge: Joins across services require API calls or replicated read models. Accept eventual consistency or build query-specific aggregations.
Shared databases between services are a common anti-pattern—they recreate monolith coupling with network overhead.
Bulkhead
Named after ship compartments, a bulkhead isolates resources so failure in one area does not consume all capacity.
Examples:
- Separate thread pools for different downstream calls
- Dedicated connection pools per service dependency
- Rate limits per tenant
If one slow dependency cannot exhaust all threads, the rest of the system keeps responding.
Sidecar and Service Mesh
A sidecar is a helper process deployed alongside each service instance (often as a container in the same pod). Sidecars handle TLS, metrics, retries, and traffic routing.
A service mesh is a network of sidecars with centralized control. Istio and Linkerd are common choices on Kubernetes.
Meshes solve cross-cutting concerns without polluting application code. They also add operational overhead—justified at scale, often overkill for five services.
Anti-patterns to avoid
Distributed monolith. Services that must deploy together and share a database are a monolith with extra latency.
Chatty services. A single user request triggering 15 synchronous internal calls will feel slow. Aggregate, cache, or redesign boundaries.
No observability. Distributed tracing (OpenTelemetry), correlated logs, and service-level metrics are not optional. Without them, debugging is guesswork.
Premature decomposition. Splitting before you understand domain boundaries leads to services that constantly change together anyway.
Choosing patterns for your system
Start with the basics almost every microservices deployment needs:
- API Gateway or BFF for client-facing routing
- Service discovery (or a platform that provides it)
- Circuit breakers and timeouts on synchronous calls
- Centralized logging and distributed tracing
Add sagas, CQRS, or event sourcing when you hit specific consistency or read/write scaling problems—not before.
FAQ
How many microservices is too many?
There is no magic number. If teams spend more time on coordination and infrastructure than features, you may have over-split. Conway's Law suggests aligning services with team boundaries.
Should microservices communicate via REST or messaging?
Use synchronous REST or gRPC for queries and request/response flows where the caller needs an immediate answer. Use asynchronous messaging (Kafka, SQS, RabbitMQ) for events, notifications, and workflows that tolerate delay.
How do I handle authentication across services?
Centralize authentication at the gateway. Services receive validated tokens (JWT) or internal identity headers. Avoid each service implementing its own login flow.
Is Kubernetes required for microservices?
No. Services can run on ECS, Cloud Run, VMs with Consul, or serverless functions. Kubernetes is popular because it bundles discovery, scaling, and deployment—but it is not the only path.
What is the first pattern I should implement?
Timeouts and circuit breakers on every outbound call. They cost little and prevent the most common production incidents in distributed systems.
Comments
Loading comments…