Baraklabs

15 Microservices Design Patterns Explained with Real Examples

From API Gateway to Anti-Corruption Layer — 15 patterns explained through one food delivery platform, with trade-offs and when to use each.

The Problem Microservices Solve

Imagine you are building a food delivery platform like Swiggy or Zomato. In the early days, four engineers build it as one monolith: one codebase, one database, one server. It works perfectly — you ship fast and everyone understands the whole system.

Two years later: 150 engineers, 2 million lines of code. Deploying a fix to the restaurant listing page requires coordinating with the payments team because they share a database table. A bug in the notification module takes down the entire order system. One server cannot handle peak-hour traffic.

Microservices break that monolith into small, independently deployable services — each owning one business capability. All 15 patterns in this guide are explained through one consistent example: a food delivery platform with six services:

User Service

accounts, login, saved addresses

Restaurant Service

menus, availability, ratings

Order Service

creates and tracks order lifecycles

Payment Service

payment processing and refunds

Notification Service

push notifications, SMS, email

Delivery Tracking Service

real-time rider GPS and ETA

Pattern 01

API Gateway

A single entry point that receives every client request, routes it to the right service, and stitches results back together. It also handles authentication, rate limiting, SSL termination, and request routing — so individual services never repeat that work.

The Problem

Without an API Gateway, every client must know the internal address of every service, and each service must implement its own auth logic independently. Adding a new service means updating every client.

How It Works

1.Client sends a single request to the API Gateway
2.Gateway authenticates the JWT token
3.Gateway fans out sub-requests to Restaurant, Order, and User services in parallel
4.Gateway stitches all responses and returns one unified result

Real-World Example

When you open Swiggy, one call to the API Gateway fans out to three services simultaneously — returning a complete home screen with restaurants, active orders, and saved addresses in under a second.

When to Use

  • Clients need to call multiple services in one user interaction
  • You want centralized auth, logging, and rate-limiting in one place
  • Building a public API that aggregates data from many internal services

Trade-offs / When NOT to Use

  • If you only have 1–2 services — the overhead is not worth it
  • When ultra-low latency is critical — every extra hop adds milliseconds
  • The Gateway should never contain business logic
Pattern 02

Service Discovery

Services register themselves in a registry (Eureka or Consul) on startup and send periodic heartbeats. Other services query the registry to find healthy instances instead of hardcoding IP addresses.

The Problem

In dynamic cloud environments, services scale from 1 to 20 instances within seconds and their IP addresses change constantly. Hardcoded addresses mean half your new capacity goes unused and crashes when the hardcoded instance dies.

How It Works

1.Payment Service starts and registers: 'PaymentService at 10.0.0.42:8080'
2.Eureka marks it healthy based on periodic heartbeats
3.Order Service queries Eureka: 'Give me a healthy Payment instance'
4.Eureka returns a list; Order Service picks one (round-robin or weighted)
5.If a Payment instance dies, Eureka detects the failed heartbeat and removes it within seconds

Real-World Example

During a Diwali sale, DevOps spins up 8 new Payment Service instances. They self-register in Consul. Within seconds, the API Gateway routes to all 10 instances automatically — no config changes, no deployments needed.

When to Use

  • Dynamic environments where services scale up and down
  • Container orchestration like Kubernetes or Docker Swarm
  • Services need to call each other and instance count changes frequently

Trade-offs / When NOT to Use

  • For small deployments with fixed infrastructure — just use config files
  • Don't over-engineer early-stage systems with a registry before you need it
Pattern 03

Circuit Breaker

Watches how many requests to a downstream service are failing. When failures cross a threshold, it trips to OPEN and returns fast failures immediately — instead of making callers wait 30 seconds for a timeout. Three states: CLOSED (normal), OPEN (blocked), HALF-OPEN (testing recovery).

The Problem

Without a Circuit Breaker, when Payment Service is down, every Order Service request waits 30 seconds. 1,000 simultaneous orders means 1,000 threads hanging, consuming memory. Order Service itself crashes — even though it was Payment that was sick. This is called a cascading failure.

How It Works

1.CLOSED: all requests pass through normally
2.After 5 failures in 10 seconds, the breaker trips to OPEN
3.OPEN: all calls return an immediate 'Service unavailable' — no waiting
4.After 30 seconds, enters HALF-OPEN: one probe request gets through
5.If the probe succeeds, resets to CLOSED; if not, stays OPEN another interval

Real-World Example

During peak hours, if the credit card payment service slows down, Resilience4j opens the circuit after 5 failures. The app immediately shows: 'Credit card unavailable. Try UPI or Cash on Delivery.' The order continues without delay.

When to Use

  • Calling external APIs or slow downstream services
  • Any synchronous service-to-service call in production
  • You need to isolate failures and prevent cascading breakdowns

Trade-offs / When NOT to Use

  • Don't use it as a substitute for actually fixing reliability in the downstream service
Pattern 04

Database per Service

Each microservice owns its own database. No other service is allowed to connect to it directly. If Service B wants data from Service A's database, it must ask via an API call. Each service can also choose the right database technology for its needs.

The Problem

In a monolith with one shared database, changing the Users table schema breaks the Order service too. When the DBA adds an index to optimize restaurant queries, it locks the table and slows simultaneous order writes.

How It Works

1.User Service → user_db (PostgreSQL): users, addresses, preferences
2.Order Service → order_db (MongoDB): order states and history as documents
3.Payment Service → payment_db (PostgreSQL): ACID transactions for financial records
4.Delivery Service → Redis: real-time geospatial indexes for 10,000 riders
5.If Order needs a user address, it calls User Service's API — never queries user_db directly

Real-World Example

Swiggy's Delivery Service stores real-time GPS coordinates for 10,000 riders. This is a terrible fit for PostgreSQL but perfect for Redis geospatial indexing. Order Service calls Delivery Service's API for rider location and doesn't care what database is behind it.

When to Use

  • Teams need to deploy services independently without database coordination
  • Different services have genuinely different storage requirements
  • You need to scale individual service databases without affecting others

Trade-offs / When NOT to Use

  • If you need complex multi-table joins across services — very hard with separate databases
  • Small systems where the overhead of multiple databases is not justified
Pattern 05

Event-Driven Architecture

Instead of Service A calling Service B and waiting for a response, Service A publishes an event to a message broker (Kafka). Multiple downstream services subscribe to that event and process it independently — decoupling producers from consumers completely.

The Problem

Without async messaging, placing an order requires Order Service to synchronously call Payment (2s), Notification (1s), and Restaurant (1s). Total: 4 seconds per order, tightly coupled. If any one service fails, the whole thing fails.

How It Works

1.Customer places order → Order Service publishes 'order.placed' to Kafka
2.Payment Service consumes 'order.placed', charges the card, publishes 'payment.completed'
3.Restaurant Service consumes 'order.placed', confirms kitchen, publishes 'restaurant.confirmed'
4.Notification Service consumes 'payment.completed' and sends a push notification
5.Delivery Service consumes 'restaurant.confirmed' and assigns a nearby rider
6.Each step is independent — total time is the slowest step, not the sum of all steps

Real-World Example

When you tap 'Place Order' on Swiggy, that one event fans out to five systems simultaneously: payment, restaurant notification, push notification, rider assignment, and analytics. None waited for the others.

When to Use

  • Workflows involving multiple services reacting to a single trigger
  • When you need resilience — if Notification is down, events stay in the queue
  • High-throughput systems where synchronous calls would become a bottleneck

Trade-offs / When NOT to Use

  • When you need an immediate synchronous response (e.g., 'Is this card valid?')
  • Very simple two-service interactions — a direct REST call is simpler
  • Cannot tolerate eventual consistency — async means services may be briefly out of sync
Pattern 06

Saga Pattern

In a monolith you used one database transaction to roll everything back. In microservices each service has its own database — there is no single DB to roll back. The Saga pattern coordinates a sequence of local transactions: if a step fails, compensating transactions undo all previous steps.

The Problem

Without Saga, a distributed order placement might deduct payment successfully and then fail when the restaurant rejects the order — leaving the customer charged with no food. There is no two-phase commit spanning multiple microservice databases.

How It Works

1.Order Service creates a pending order and publishes 'order.created'
2.Payment Service charges the card and publishes 'payment.completed'; on failure → 'payment.failed' → Order Service cancels the order
3.Restaurant Service confirms availability and publishes 'restaurant.confirmed'; on rejection → triggers a refund
4.Delivery Service assigns a rider and publishes 'rider.assigned'
5.If any step fails, compensating transactions run backward: cancel rider → refund payment → cancel order

Real-World Example

Amazon uses the Saga pattern for order fulfillment. If a warehouse says an item is out of stock after your card was charged, a compensating transaction automatically refunds your card and sends an apology email — no manual intervention.

When to Use

  • Multi-step business transactions spanning multiple services
  • When you need eventual consistency without distributed ACID transactions
  • Long-running workflows where steps may take minutes or hours

Trade-offs / When NOT to Use

  • If you can redesign to fit everything in one service's transaction — do that first
  • When immediate consistency is non-negotiable — Sagas are eventually consistent
Pattern 07

CQRS

Command Query Responsibility Segregation: separate the code path that writes data from the code path that reads it. Commands change state; queries read state. This lets you optimize writes for correctness (PostgreSQL with ACID) and reads for speed (Elasticsearch with projections).

The Problem

Without CQRS, you use the same database for reads and writes, and they compete for the same resources. Writing an order needs normalized relational tables and ACID guarantees. Reading order history needs lightning-fast, denormalized, searchable data.

How It Works

1.Customer places order → Command → Order Write Service validates and saves to PostgreSQL
2.Order Service publishes 'order.created' event to Kafka
3.Read Model Projector consumes the Kafka event and updates Elasticsearch with a display-ready summary
4.Customer checks order history → Query → Order Read Service returns fast Elasticsearch results
5.Writes and reads never contend for the same database resources

Real-World Example

Writes land in PostgreSQL with strict schemas and ACID guarantees. Reads are served from a pre-aggregated Elasticsearch projection — millisecond lookups even across millions of records.

When to Use

  • Read and write loads are dramatically different in volume or complexity
  • Domain has complex business logic on writes but simple display needs on reads

Trade-offs / When NOT to Use

  • Simple CRUD applications — CQRS adds significant complexity not worth the overhead
Pattern 08

Backends for Frontends (BFF)

One dedicated backend per client type. Instead of one API Gateway compromising for everyone, you have BFF Mobile and BFF Web — each built perfectly for its client. Mobile gets a tiny compressed response; web gets a rich detailed one.

The Problem

Without BFF, your single API Gateway returns a 4KB JSON response to every client. Mobile apps on slow 4G connections download 3.5KB of unnecessary fields on every screen load, wasting bandwidth and battery.

How It Works

1.BFF Mobile: aggregates only the fields the mobile screen needs, compressed for 4G
2.BFF Web: rich, detailed 50-field response for the desktop dashboard
3.Each BFF independently calls the internal microservices it needs
4.Mobile users get thumbnails and download toggles; web users get full metadata and deep navigation

Real-World Example

Netflix uses BFF for every device type. The TV app API needs high-resolution artwork and surround-sound metadata. The mobile app needs thumbnails, download toggles, and notification settings. One BFF per device, each optimized for its client.

When to Use

  • Multiple client types with significantly different data needs

Trade-offs / When NOT to Use

  • If all clients need the same data — you're just duplicating logic
  • Small products with only one client type
Pattern 09

Sidecar / Service Mesh

A small proxy container (typically Envoy) runs alongside every service container in the same pod. Your service contains pure business logic. The sidecar handles mTLS encryption, retry logic, distributed tracing, and metrics. Connecting all sidecars via a Control Plane (Istio) creates a Service Mesh.

The Problem

Without a Service Mesh, retry logic is written in Java for Order Service, Python for Delivery Service, and Go for Payment Service — three codebases, three bugs to fix, three places to update when the retry policy changes.

How It Works

1.Each service pod runs alongside an Envoy sidecar proxy
2.All inbound/outbound traffic passes through the sidecar automatically
3.Sidecar handles mTLS encryption, retries, timeouts, and distributed tracing
4.Istio Control Plane manages all policies centrally across every sidecar
5.Zero application code changes needed — it's entirely infrastructure

Real-World Example

Swiggy deploys 6 services written in Java, Python, and Go. Instead of each team implementing retry logic separately, Envoy sidecars handle it uniformly. One policy change in Istio updates all services instantly.

When to Use

  • Many services in multiple languages needing uniform cross-cutting behavior
  • Fine-grained traffic control (canary releases, A/B testing) without code changes

Trade-offs / When NOT to Use

  • Small setups with 2–3 services — the operational overhead of a mesh is significant
  • Teams without Kubernetes expertise — service meshes are complex to operate
  • Every sidecar hop adds a small overhead (typically under 1ms, but not zero)
Pattern 10

Strangler Fig

Place a routing facade (API Gateway or reverse proxy) in front of the existing monolith. New features are built as microservices. Existing features are migrated one at a time. The facade routes requests to either the new microservice or the monolith depending on what's ready.

The Problem

A 5-year-old Java monolith with 2 million lines of code. A full rewrite would take 18 months with no new features shipped. Meanwhile competitors keep shipping. How do you modernize without stopping the business?

How It Works

1.Deploy a Strangler Facade (Nginx or API Gateway) in front of the monolith
2.Initially all traffic goes straight to the monolith — nothing changes for users
3.Build 'User Service' as a standalone microservice and test it in staging
4.Configure the Facade: 'Route /api/users/* to the new User Service'
5.Monitor, fix issues, and gradually migrate more routes
6.Eventually all routes go to microservices — the monolith is decommissioned

Real-World Example

Swiggy places an Nginx facade in front of their monolith. Each quarter, one module is extracted. User Service first, then Payments, then Restaurant — while users see zero disruption and the team ships features the whole time.

When to Use

  • Migrating a legacy monolith to microservices without a risky big-bang rewrite
  • Business must keep running and shipping features during migration
  • Teams want to move incrementally rather than all at once

Trade-offs / When NOT to Use

  • Not suitable for MVPs or small applications where the overhead is unjustified
Pattern 11

Bulkhead

Isolate resources — thread pools, connection pools, memory — per service or feature. If one feature exhausts all its threads, other features retain their own dedicated pools and continue working.

The Problem

With a single shared pool of 200 threads, a Search feature sending 300 concurrent requests to a slow Elasticsearch cluster consumes all 200 threads. An Order request arrives and finds no threads available — orders fail even though the Order backend is perfectly healthy.

How It Works

1.Assign dedicated thread pools to each logical group of work
2.Order Pool: 50 threads
3.Payment Pool: 20 threads
4.Search Pool: 80 threads
5.Even if Search exhausts all 80 of its threads, Order Pool still has its 50 untouched

Real-World Example

During a sale, Swiggy's Search overloads and its pool exhausts its 80 threads. With Bulkheads, Order Pool still has 50 threads available. Orders continue processing while Search degrades gracefully.

When to Use

  • Multiple consumers of shared resources where one heavy user could starve others
  • Critical paths like order completion must be protected from non-critical paths like search

Trade-offs / When NOT to Use

  • When all operations are equal priority and resource usage is predictable
  • Very small systems where managing multiple pools adds unnecessary overhead
Pattern 12

Outbox Pattern

Instead of writing to the database AND publishing to Kafka in two separate operations, write to both the main table and an outbox table in one atomic transaction. A relay process (Debezium) reads the outbox and publishes to Kafka — guaranteeing at-least-once delivery even if Kafka is briefly down.

The Problem

You save an order to the database, then Kafka is briefly down. The database has the order, but nobody downstream knows about it. Payment never happens. The event is silently lost — a common failure mode in distributed systems.

How It Works

1.Order Service begins a database transaction
2.Inserts the order into the orders table
3.Also inserts a row into outbox table: {event_type: 'order.placed', published: false}
4.Transaction commits atomically — both rows save, or neither does
5.Debezium watches the outbox table via CDC (change data capture from the transaction log)
6.Debezium publishes the event to Kafka and marks the row as published
7.If Kafka is down, the outbox row stays and Debezium retries until it succeeds

Real-World Example

Swiggy's Order Service writes an order and an outbox entry in one transaction. Even if Kafka is briefly down at 2am, Debezium picks up the unsent event on restart and publishes it — no lost orders, no manual intervention.

When to Use

  • Guaranteed event delivery — lost events are unacceptable
  • Implementing Saga and needing reliable event propagation between steps

Trade-offs / When NOT to Use

  • Added CDC or polling infrastructure — too high complexity for small projects
  • Downstream consumers must handle duplicate events — fix idempotency before deploying Outbox
Pattern 13

Retry with Exponential Backoff

When a call fails, wait and try again — but don't retry in a tight loop. Double the wait time with each attempt (1s, 2s, 4s, 8s) and add random jitter to prevent all retrying clients from hammering the recovering service at the exact same moment (the 'thundering herd' problem).

The Problem

Payment Service occasionally returns 503 for 200ms during garbage collection. Without retries, 5% of payments fail unnecessarily. With a naive tight retry loop, all retried requests arrive simultaneously and extend the outage from 200ms to 10 seconds.

How It Works

1.Attempt 1 fails → Wait 1s + random jitter (0–500ms)
2.Attempt 2 fails → Wait 2s + jitter
3.Attempt 3 fails → Wait 4s + jitter
4.Attempt 4 succeeds — total extra wait ~7s, but the payment completes
5.If all attempts fail, raise an exception and let the Circuit Breaker take over

Real-World Example

Swiggy's Order-to-Payment call gets a 503. Resilience4j waits 1.3s (1s + jitter) and retries — succeeds. The customer sees a slight delay but no error. Without this, 5% of orders during GC pauses would fail hard.

When to Use

  • Handling transient failures — timeouts, 503s, network glitches that resolve on their own
  • When the operation is idempotent (safe to retry without side effects)

Trade-offs / When NOT to Use

  • Business logic errors like 400 Bad Request — retrying a malformed request wastes time
  • Non-idempotent operations — never blindly retry a payment deduction
  • When a Circuit Breaker is already open — don't retry when the breaker says the service is down
Pattern 14

Ambassador

A helper container that runs alongside a service and handles all outbound communication complexity: connection pooling, retry logic, logging, OAuth refresh, and rate limiting. Your main service makes a simple localhost call to the Ambassador; it handles the real external call.

The Problem

Order Service needs to call Payment Gateway, Google Maps, and an SMS provider. Each has different authentication, rate limits, and retry requirements. Implementing all of this inside Order Service makes it bloated with non-business logic — and different teams implement it inconsistently.

How It Works

1.Order Service and Ambassador container run in the same Kubernetes pod
2.Order Service sends a simple HTTP request to localhost:8080 (the Ambassador)
3.Ambassador translates this into an authenticated, rate-limited call to the external Payment API
4.Ambassador handles OAuth token refresh, retries on failure, and logs the response
5.Order Service code contains zero authentication, retry, or rate-limiting logic

Real-World Example

Swiggy's Ambassador handles authentication to three external APIs from one sidecar — OAuth refresh, rate limiting, and retry are all managed in one place. The Order Service just calls localhost and gets back a clean result.

When to Use

  • Legacy services that cannot easily adopt retry or auth logic in their codebase
  • You want a consistent proxy for all outbound external API calls
  • Multi-language environments where one team owns all external communication patterns

Trade-offs / When NOT to Use

  • If the Service Mesh already handles your outbound call needs — Ambassador is redundant
  • When Ambassador container complexity outweighs the benefit for simple integrations
  • For internal service-to-service calls where the Service Mesh already covers it
Pattern 15

Anti-Corruption Layer (ACL)

A translation layer that sits between your clean domain model and a legacy or foreign system. It translates the legacy system's messy model into your domain's language and back — preventing legacy concepts from leaking into and corrupting your clean codebase.

The Problem

Swiggy acquires a legacy restaurant POS using 1990s concepts: MENU_ITEM_CODE, SEAT_COUNT, TAX_BUCKET, TBL_SRV_CHG. Without an ACL, Swiggy's modern codebase gets polluted with POS terminology and engineers spend hours decoding what TBL_SRV_CHG means.

How It Works

1.Restaurant Service sends clean request: {menuItemId: 'pizza-123', restaurantCapacity: 50}
2.ACL translates to POS format: {MENU_ITEM_CODE: 'PIZZA_123', SEAT_COUNT: 50}
3.ACL sends the translated request to the legacy POS system
4.POS responds: {STATUS_CD: 'OK', ORD_TOTAL: 450.00}
5.ACL translates back: {status: 'confirmed', totalAmount: 450.00}
6.Restaurant Service only ever sees clean, modern domain concepts

Real-World Example

During Swiggy's legacy POS integration, the ACL acts as a translator in both directions. The Restaurant Service team never encounters MENU_ITEM_CODE or TBL_SRV_CHG — the ACL handles all the mapping and the old mess never leaks into the new system.

When to Use

  • Integrating with legacy systems, third-party APIs, or systems with incompatible domain languages
  • When you cannot modify the external system and its concepts are fundamentally incompatible with yours
  • During incremental legacy migration — pairs naturally with the Strangler Fig pattern

Trade-offs / When NOT to Use

  • When the external system's model is actually a good fit for your domain — just use it directly
  • Simple integrations where a thin client wrapper is sufficient

How Patterns Work Together

In a real production system, patterns are never used in isolation. Here is the journey of a single order on the food delivery platform — and how seven or more patterns collaborate simultaneously on that one button tap.

1

1. Tap 'Place Order'

API Gateway (P01) receives the request, authenticates the JWT token, and routes to Order Service. Service Discovery (P02) tells the Gateway which healthy Order Service instance to use.

2

2. Payment call with resilience

Order Service calls Payment Service through a Circuit Breaker (P03). If Payment is slow tonight, the breaker trips and returns a graceful fallback. Retry with Exponential Backoff (P13) handles transient network glitches.

3

3. Atomic write + guaranteed event

Order Service writes to its own database (P04 — Database per Service) and simultaneously writes to an Outbox table (P12) — one atomic transaction ensuring the event cannot be lost even if Kafka is briefly down.

4

4. Event fans out to 5 systems

The Outbox Relay publishes 'order.placed' to Kafka (P05 — Event-Driven Architecture). Payment, Restaurant, Notification, Delivery, and Analytics all react independently and in parallel.

5

5. Distributed transaction with rollback

The Saga Orchestrator (P06) coordinates the multi-step transaction: Payment charges the card, Restaurant confirms, Delivery assigns a rider. If any step fails, compensating transactions undo the previous ones.

6

6. Tailored response to mobile

The mobile BFF (P08) aggregates status from Order, Payment, and Delivery Services and returns a lightweight response to the mobile app — without overwhelming it with data it doesn't need.

7

7. Transparent infrastructure layer

Every service-to-service call passes through an Envoy Sidecar (P09 — Service Mesh): encrypts traffic (mTLS), tracks the request path for debugging, and retries on failure — zero application code changes. When a service calls the legacy restaurant POS, the Anti-Corruption Layer (P15) translates between modern and legacy domain languages.

The art of microservices architecture is not knowing all 15 patterns individually — it is knowing which combination to apply to the specific problems your system is actually facing right now. Start with API Gateway, Service Discovery, and Circuit Breaker. Add more only as your system grows and the problems they solve become real for your team.

Recommended Blogs