Skip to content

Microservices Design Patterns

Core Concept LearningJuly 1, 20266 min readUpdated July 21, 2026

This guide is for backend engineers who know HTTP and database transactions but need to decide where six microservice patterns fit. By the end, you can trace one checkout, configure representative boundaries, and recover duplicate delivery, timeout, stale read, and compensation failures.

The running example is checkout across mobile/web, Order, Payment, Inventory, and Email. For load balancers, brokers, and Kubernetes as one deployment path, use the Microservices Architecture Blueprint instead of turning this pattern guide into an infrastructure catalog.

Choose synchronous and asynchronous boundaries with the service communication protocol map. For saga, outbox, and choreography mechanics, continue with event-driven architecture patterns.

Six patterns: edge, discovery, reads/writes, async, data ownership
Six patterns: edge, discovery, reads/writes, async, data ownership

API Gateway

Mobile and laptop clients should not hold five service URLs. An API Gateway is the single entry point: it routes to Microservice A, B, or C and centralizes auth, rate limiting, and request shaping.

When to use: many external clients and shared cross-cutting concerns. When not to: a single internal service with one consumer — a gateway adds hop latency without buying much. Keep business rules out of the gateway; it is a door, not a domain service.

Clients → API Gateway → Services A / B / C
Clients → API Gateway → Services A / B / C

Quick reference

  • Clients → gateway → Service A / B / C.
  • Centralizes JWT auth, rate limits, TLS, logging.
  • Hides internal topology from the outside world.
  • Kong, Envoy, AWS API Gateway, Azure APIM are common picks.
  • Avoid stuffing domain logic into the gateway.

Remember this

A gateway is a door, not a domain service — business rules stuffed into it are the first thing to regret when a second client type needs different behavior.

Backends for Frontends (BFF)

Mobile apps and web apps rarely want the same payload. BFF gives each client type its own backend: Mobile Apps → Mobile BFF, Web Apps → Web BFF, both talking to the same Microservice A/B/C behind them.

The mobile BFF returns compact fields for bandwidth; the web BFF returns richer nested views. When to use: divergent UX and payload needs. When not to: one client type — prefer a thin gateway alone.

Mobile → Mobile BFF · Web → Web BFF · shared services behind
Mobile → Mobile BFF · Web → Web BFF · shared services behind

Quick reference

  • One BFF per client family (mobile, web, admin, partner).
  • BFF aggregates and shapes; domain services stay shared.
  • Stops over-fetch on mobile and under-fetch on web.
  • Keep BFFs thin — no duplicated business rules.
  • Often sits with or behind the API Gateway.

Remember this

A BFF stops mobile over-fetching and web under-fetching by shaping the payload per client family — duplicating business rules inside it instead of keeping it thin defeats the whole point.

Service Discovery

Instances come and go. The Client hits the API Gateway; Services A/B/C register with a Service Registry; the gateway (or client) looks up healthy addresses before routing.

When to use: dynamic containers/VMs with changing IPs. When not to: a static two-service setup you never redeploy — DNS or config may be enough. On Kubernetes, CoreDNS often is your registry.

Register → Gateway looks up → route to healthy instance
Register → Gateway looks up → route to healthy instance

Quick reference

  • Register on start; deregister on shutdown.
  • Health checks drop dead instances from the registry.
  • Gateway queries registry, then routes the client request.
  • Consul, Eureka, etcd — or Kubernetes DNS.
  • Never hard-code host:port for production peers.

Remember this

A hard-coded host:port for a production peer breaks the moment that instance is redeployed — health checks dropping dead instances from the registry is what keeps the gateway routing to something alive.

CQRS (Command Query Responsibility Segregation)

CQRS splits writes and reads. The Client sends commands to a Command Service/Model that updates Storage, and queries to a Query Service/Model that reads — often from a denormalized view of the same or separate store.

When to use: read/write ratios diverge (e.g. catalog reads dwarf checkout writes). When not to: simple CRUD — two models are overhead you will regret. Expect eventual consistency between command and query sides.

Command path writes · Query path reads · shared or split storage
Command path writes · Query path reads · shared or split storage

Quick reference

  • Commands mutate; queries only read.
  • Scale and optimize each path independently.
  • Shared storage or separate read store both work.
  • Pair with events to project read models.
  • Skip CQRS until measurement shows the need.

Remember this

A stale read in a CQRS projection is visible evidence, not proof the command failed — expose projectedAt so the client can tell staleness from failure, and skip the two-model split entirely for simple CRUD.

Event Driven

A Producer publishes to a Broker (Kafka, RabbitMQ, …); Consumers (Service A/B/C) receive and react — inventory update, email, analytics — without the producer waiting.

When to use: fan-out after checkout, resilience when a downstream is down. When not to: the user must see a synchronous result in the same request (use sync call or hybrid). Consumers must be idempotent; brokers often deliver at-least-once.

Producer publishes · Broker fans out · Consumers receive
Producer publishes · Broker fans out · Consumers receive

Quick reference

  • Publish facts (OrderPlaced), not vague commands.
  • Broker decouples producers from consumer uptime.
  • Kafka for streams; RabbitMQ for routing; SQS for managed queues.
  • Dead-letter queues for poison messages.
  • Combine with sagas for multi-step workflows.

Remember this

A broker that delivers at-least-once means every consumer has to be idempotent — an insert guarded by ON CONFLICT DO NOTHING is what turns a redelivered event into a safe no-op instead of a double reservation.

Database per Service

Order, Payment, and Inventory services each own a private database. Schema changes in one do not break the others. Cross-service data moves via APIs or events — never shared tables.

When to use: independent teams and deploy cadence. When not to: a modular monolith still sharing one Postgres with clear module boundaries — extract DBs when ownership and scale demand it. No cross-service JOINs; compose with APIs or CQRS read models.

Order · Payment · Inventory — each with its own database
Order · Payment · Inventory — each with its own database

Quick reference

  • One service → one data store (logical ownership).
  • No shared schemas or foreign keys across services.
  • Enables independent deploy and polyglot persistence.
  • Trade-off: eventual consistency across boundaries.
  • Shared databases quietly undo microservice autonomy.

Remember this

A shared database quietly undoes microservice autonomy the moment one team's schema change breaks another's query — no cross-service JOINs means composing through APIs or CQRS read models instead.

Recover One Checkout Failure End to End

Order writes OrderPending and an outbox row in one local transaction, then returns 202 with operation key checkout-42. The publisher sends PaymentRequested; Payment charges with that key. Its response times out after the provider commits, and the broker redelivers the event. Payment first queries the provider by key, finds the existing charge, records PaymentSucceeded, and publishes one logical success. Inventory’s processed_messages guard makes the duplicate delivery a no-op.

The Order read projection is now 20 seconds behind, so the UI still shows pending. That stale read is not a reason to charge again: the response includes projectedAt, and the UI polls the operation resource. When the projector catches up, status becomes paid. If inventory instead reports insufficient stock, the saga issues RefundRequested; Payment refunds with compensation key checkout-42:refund and Order becomes cancelled only after RefundSucceeded.

If the refund times out, the saga remains compensating and queries by the compensation key before retrying. Alert on age in each nonterminal state. Recovery is complete only when provider charge/refund records, service databases, and the read projection reconcile to one final outcome; a dead-letter queue alone does not repair business state.

Zoom into one saga compensation after payment fails
Zoom into one saga compensation after payment fails

Quick reference

  • Duplicate: broker redelivery → processed-message key → no second reservation.
  • Timeout: unknown provider outcome → status lookup by operation key → same-key retry only if absent.
  • Stale read: projection lag → expose freshness and poll operation state → never repeat the command.
  • Compensation: inventory rejection → keyed refund → verify provider and service records converge.
  • Alert on aging pending/compensating states; DLQ messages still require domain reconciliation.
Pseudo-code: saga transition contract
1# PSEUDO-CODE — durable transitions, not an in-memory loop2on PaymentTimeout(key):3    charge = provider.find_charge(key)4    emit PaymentSucceeded(key, charge.id) if charge else RetryPayment(key)5 6on InventoryRejected(key):7    emit RefundRequested(key + ":refund")8 9on RefundTimeout(compensation_key):10    refund = provider.find_refund(compensation_key)11    emit RefundSucceeded(compensation_key) if refund else RetryRefund(compensation_key)
Recovery verification checklist
1# After replay/recovery, query all owners for checkout-42:2# provider charges with key        == 13# inventory reservations           == 04# provider refunds with :refund key == 15# order status                      == "cancelled"6# read projection status            == "cancelled"7# saga nonterminal age              == 0

Remember this

A dead-letter queue alone does not repair business state — recovery is complete only when provider records, service databases, and the read projection all reconcile to the same final outcome for checkout-42.

How the six patterns combine

On checkout, Gateway + BFF shape the client request, discovery finds healthy Order/Payment instances, database per service keeps order and payment schemas separate, CQRS may accelerate order-history reads, and events notify Inventory and Email after payment without blocking the HTTP response.

Start with gateway and database per service. Add BFF when mobile and web diverge, discovery when IPs churn, events when fan-out hurts sync calls, and CQRS when read load dwarfs writes.

How they combine on a checkout path
How they combine on a checkout path

Quick reference

  • Gateway + BFF = edge.
  • Discovery + events = runtime resilience.
  • CQRS + DB-per-service = data scale and ownership.
  • Measure before adding the next pattern.
  • Practice: mark which three you would ship first for your product.

Remember this

Gateway and database-per-service are the starting pair — BFF, discovery, events, and CQRS each get added only once mobile/web diverge, IPs churn, sync fan-out hurts, or reads dwarf writes, in that measured order.

Key takeaway

These six patterns are complementary, not mandatory. Add a gateway for a shared edge, discovery for changing instances, BFFs for materially different clients, CQRS for separately justified read/write models, events for asynchronous fan-out, and database-per-service for real ownership boundaries.

Practice (30 min): run a local checkout handler with a fake provider keyed by checkout-42. Deliver the same payment event twice, force the first response to time out after recording the charge, delay the read projection, then reject inventory and time out the refund response. Assert the five recovery-checklist lines above. Save the injected events and final queries as a repeatable integration test.

Share:

Related Articles

In high-concurrency microservices architectures, preventing race conditions when multiple stateless worker instances acc

Read

System design interviews evaluate a candidate's ability to architect scalable, resilient, and cost-effective distributed

Read

Decoupling microservices using Event-Driven Architecture (EDA) requires choosing an asynchronous messaging backbone. Eng

Read

Keep learning

Follow a structured path or browse all courses to go deeper.