Microservices Design Patterns Every Backend Developer Should Know

Stackademic

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.

ApproachExamples
Client-side discoveryEureka, Consul — client queries registry, picks instance
Server-side discoveryKubernetes DNS, AWS ALB — platform routes to healthy instance
Service meshIstio, 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.

StyleProsCons
ChoreographyLoose coupling, no coordinatorHard to trace; implicit flow
OrchestrationClear flow, easier debuggingCoordinator 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:

  1. Put a facade (gateway) in front of the monolith
  2. Build a new service for one bounded context
  3. Route that context's traffic to the new service
  4. 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:

  1. API Gateway or BFF for client-facing routing
  2. Service discovery (or a platform that provides it)
  3. Circuit breakers and timeouts on synchronous calls
  4. 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.