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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
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
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
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
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
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
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
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
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
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
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
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
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. 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. 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. 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. 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. 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. 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. 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
Read Our Blogs

Best Screen Recorder Tools: What to Look For in 2026
What makes a screen recorder good in 2026: recording quality, AI transcription, editing, and privacy — plus a checklist before you choose one.

How to Get Your First 10 Paying Users: A Step-by-Step Playbook for B2B and B2C SaaS Founders
Why the first ten matter more than the next ten thousand - and exactly how to find them.

