Microservices Communication Patterns — Sync, Async, Event-Driven, Saga
In this tutorial, you'll learn about Microservices Communication Patterns. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Microservices communication patterns define how distributed services exchange data, with synchronous patterns using direct HTTP calls and asynchronous patterns using message brokers to decouple services and improve resilience.
What You'll Learn
By the end of this tutorial, you will understand synchronous vs asynchronous communication, implement event-driven patterns with message brokers, orchestrate distributed transactions using the saga pattern, and handle service discovery and failure in microservice architectures.
Why It Matters
Choosing the wrong communication pattern creates tight coupling, cascading failures, and data inconsistency across services. Doda Browser uses asynchronous event-driven communication between its search indexing, caching, and analytics Microservices, ensuring that a failure in analytics never blocks search results from being served.
Real-World Use
An e-commerce platform with separate services for orders, inventory, payments, and shipping. When an order is placed, the order service publishes an OrderPlaced event. Inventory, payment, and shipping services consume this event independently — if shipping is down, orders are still accepted and fulfilled when shipping recovers.
Synchronous vs Asynchronous
flowchart LR
subgraph "Synchronous (Tight Coupling)"
S1[Service A] -->|"HTTP Request"| S2[Service B]
S2 -->|"HTTP Response"| S1
S1 -.->|"Blocked while waiting"| WAIT[Waiting...]
end
subgraph "Asynchronous (Decoupled)"
S3[Service A] -->|"Publish Event"| B[Message Broker]
B -->|"Consume Event"| S4[Service B]
B -->|"Consume Event"| S5[Service C]
end
style B fill:#f90,color:#fff
Synchronous HTTP Communication
// sync-communication.js
// Synchronous HTTP communication between services
const Express = require('Express');
// ── Order Service ──
const orderApp = Express();
orderApp.use(Express.JSON());
const orders = [
{ id: 'ORD-001', userId: 1, productId: 'P1', quantity: 2, status: 'pending' },
];
// GET /orders/:id — fetch order with user details from User Service
orderApp.get('/orders/:id', async (req, res) => {
const order = orders.find(o => o.id === req.params.id);
if (!order) return res.status(404).JSON({ error: 'Order not found' });
try {
// Synchronous call to User Service
console.log(`[Order] Fetching user ${order.userId} from User Service...`);
const userResponse = await fetch(`HTTP://user-service:3001/users/${order.userId}`);
const user = await userResponse.JSON();
// Synchronous call to Product Service
console.log(`[Order] Fetching product ${order.productId} from Product Service...`);
const productResponse = await fetch(`HTTP://product-service:3002/products/${order.productId}`);
const product = await productResponse.JSON();
// Aggregate response
const orderDetail = {
...order,
user: { id: user.id, name: user.name, email: user.email },
product: { id: product.id, name: product.name, price: product.price },
};
console.log(`[Order] Returning aggregated order ${order.id}`);
res.JSON(orderDetail);
} catch (error) {
console.error(`[Order] Failed to fetch dependencies: ${error.message}`);
res.status(502).JSON({
error: 'Unable to fetch order details',
detail: 'A downstream service is unavailable',
});
}
});
orderApp.listen(3000, () => console.log('Order Service on :3000'));
Expected behavior: The order service makes synchronous HTTP calls to user and product services. If either service is down, the request fails with 502. The client is blocked until all calls complete. This coupling means a failure in any service propagates to all dependent services.
Event-Driven Communication with RabbitMQ
# event_driven.py
# Event-driven communication with RabbitMQ
import pika
import JSON
import time
import uuid
# ── Event Publisher (Order Service) ──
class EventPublisher:
"""Publish domain events to RabbitMQ."""
def __init__(self):
self.connection = pika.BlockingConnection(
pika.ConnectionParameters('localhost', 5672)
)
self.channel = self.connection.channel()
self.channel.exchange_declare(
exchange='orders',
exchange_type='topic',
durable=True
)
def publish_event(self, event_type, data):
"""Publish an event with the given routing key."""
event = {
'event_id': str(uuid.uuid4()),
'event_type': event_type,
'timestamp': time.time(),
'data': data,
}
self.channel.basic_publish(
exchange='orders',
routing_key=event_type,
body=JSON.dumps(event),
properties=pika.BasicProperties(
delivery_mode=2, # Persistent
content_type='application/JSON',
)
)
print(f"[Publisher] Published {event_type}: {event['event_id']}")
return event['event_id']
publisher = EventPublisher()
# Simulate order placement
publisher.publish_event('order.placed', {
'order_id': 'ORD-12345',
'user_id': 42,
'product_id': 'P-100',
'quantity': 3,
'amount': 149.99,
})
publisher.publish_event('order.placed', {
'order_id': 'ORD-12346',
'user_id': 43,
'product_id': 'P-200',
'quantity': 1,
'amount': 29.99,
})
Expected output:
[Publisher] Published order.placed: a1b2c3d4-...
[Publisher] Published order.placed: e5f6g7h8-...
Event Consumer
# event_consumer.py
# Consume domain events (Inventory Service)
import pika
import JSON
class InventoryEventConsumer:
"""Consume order events and update inventory."""
def __init__(self):
self.connection = pika.BlockingConnection(
pika.ConnectionParameters('localhost', 5672)
)
self.channel = self.connection.channel()
self.channel.exchange_declare(
exchange='orders',
exchange_type='topic',
durable=True
)
# Create Queue bound to order.placed events
result = self.channel.Queue_declare(Queue='', exclusive=True)
self.Queue_name = result.method.Queue
self.channel.Queue_bind(
exchange='orders',
Queue=self.Queue_name,
routing_key='order.placed'
)
def handle_order_placed(self, ch, method, properties, body):
"""Process order.placed event — reserve inventory."""
event = JSON.loads(body)
data = event['data']
print(f"[Inventory] Processing {event['event_type']}: {data['order_id']}")
print(f" -> Reserving {data['quantity']} units of {data['product_id']}")
# Simulate inventory check
if data['quantity'] > 10:
print(f" -> WARNING: Insufficient stock for {data['product_id']}")
else:
print(f" -> Inventory reserved successfully")
ch.basic_ack(delivery_tag=method.delivery_tag)
def start(self):
"""Start consuming events."""
self.channel.basic_consume(
Queue=self.Queue_name,
on_message_callback=self.handle_order_placed
)
print("[Inventory] Waiting for order.placed events...")
self.channel.start_consuming()
if __name__ == '__main__':
consumer = InventoryEventConsumer()
consumer.start()
Expected output:
[Inventory] Processing order.placed: ORD-12345
-> Reserving 3 units of P-100
-> Inventory reserved successfully
[Inventory] Processing order.placed: ORD-12346
-> Reserving 1 unit of P-200
-> Inventory reserved successfully
The inventory service processes events independently. If the inventory service is down when orders are placed, events remain in the Queue and are processed when it recovers. The order service never blocks waiting for inventory.
Saga Pattern for Distributed Transactions
# saga_pattern.py
# Saga Orchestration for distributed transactions
import JSON
import time
from enum import Enum
class SagaStepStatus(Enum):
PENDING = 'pending'
COMPLETED = 'completed'
COMPENSATED = 'compensated'
FAILED = 'failed'
class SagaStep:
"""A single step in a saga with its compensating action."""
def __init__(self, name, action, compensate):
self.name = name
self.action = action
self.compensate = compensate
class SagaOrchestrator:
"""Orchestrate a saga — execute steps and compensate on failure."""
def __init__(self, saga_id):
self.saga_id = saga_id
self.steps = []
self.executed_steps = []
def add_step(self, step):
"""Register a saga step."""
self.steps.append(step)
return self
def execute(self, context):
"""Execute all steps. Compensate if any step fails."""
print(f"\n[Saga {self.saga_id}] Starting saga execution")
print(f"[Saga {self.saga_id}] Context: {JSON.dumps(context)}")
for step in self.steps:
try:
print(f" -> Executing step: {step.name}")
result = step.action(context)
self.executed_steps.append(step)
print(f" -> Step '{step.name}' completed")
except Exception as e:
print(f" -> Step '{step.name}' FAILED: {e}")
self._compensate()
return False
print(f"[Saga {self.saga_id}] Saga completed successfully")
return True
def _compensate(self):
"""Execute compensating actions in reverse order."""
print(f"\n [Saga {self.saga_id}] Starting compensation...")
for step in reversed(self.executed_steps):
try:
print(f" -> Compensating step: {step.name}")
step.compensate({})
print(f" -> Step '{step.name}' compensated")
except Exception as e:
print(f" -> Compensation failed for '{step.name}': {e}")
print(f" [Saga {self.saga_id}] Compensation complete")
# ── Define saga steps for order processing ──
def reserve_inventory(context):
"""Step 1: Reserve inventory."""
print(f" Reserving inventory for order {context['order_id']}")
time.sleep(0.1)
# Simulate success
return True
def compensate_inventory(context):
"""Compensate: Release inventory."""
print(f" Releasing inventory reservation")
def Process_payment(context):
"""Step 2: Process payment."""
print(f" Charging ${context['amount']} to card ending in {context['card_last4']}")
time.sleep(0.1)
# Simulate failure to trigger compensation
if context.get('simulate_failure'):
raise Exception("Payment provider declined Transaction")
def compensate_payment(context):
"""Compensate: Refund payment."""
print(f" Issuing refund for payment")
def update_order_status(context):
"""Step 3: Update order to confirmed."""
print(f" Setting order {context['order_id']} status to 'confirmed'")
def compensate_order(context):
"""Compensate: Cancel order."""
print(f" Cancelling order {context['order_id']}")
# ── Execute saga ──
order_saga = SagaOrchestrator('saga-order-001')
order_saga.add_step(SagaStep('Reserve Inventory', reserve_inventory, compensate_inventory))
order_saga.add_step(SagaStep('Process Payment', Process_payment, compensate_payment))
order_saga.add_step(SagaStep('Update Order', update_order_status, compensate_order))
context = {
'order_id': 'ORD-12345',
'amount': 149.99,
'card_last4': '4242',
'simulate_failure': False,
}
order_saga.execute(context)
Expected output (success):
[Saga saga-order-001] Starting saga execution
-> Executing step: Reserve Inventory
-> Step 'Reserve Inventory' completed
-> Executing step: Process Payment
-> Step 'Process Payment' completed
-> Executing step: Update Order
-> Step 'Update Order' completed
[Saga saga-order-001] Saga completed successfully
Expected output (failure with simulate_failure=True):
[Saga saga-order-001] Starting saga execution
-> Executing step: Reserve Inventory
-> Step 'Reserve Inventory' completed
-> Executing step: Process Payment
-> Step 'Process Payment' FAILED: Payment provider declined transaction
[Saga saga-order-001] Starting compensation...
-> Compensating step: Process Payment
-> Compensating step: Reserve Inventory
[Saga saga-order-001] Compensation complete
The saga orchestrator ensures data consistency across services. If payment fails, inventory is released and the order is cancelled. Each step has a compensating action that undoes its effect.
Common Errors
1. Tight Coupling Through Synchronous Communication
Services that make direct HTTP calls to each other create runtime dependencies. When one service is slow or down, the caller blocks, holds connections, and cascades failures through the system. Use async communication with message brokers for cross-service interactions.
2. Ignoring Partial Failures
In a distributed system, any call can fail. Without timeouts, circuit breakers, and bulkheads, a failing downstream service causes upstream services to fail too. Use circuit breakers (e.g., Netflix Hystrix, Resilience4j) and set aggressive timeouts on all inter-service calls.
3. Not Handling Eventual Consistency
Event-driven systems are eventually consistent. A user may place an order before the inventory service has processed it. Design APIs to handle this: show "Order Pending" status, use optimistic UI updates, and notify users when the async processing completes.
4. Skipping Idempotency in Event Handlers
Events can be delivered multiple times. Without idempotency, processing an OrderPlaced event twice creates duplicate orders. Use event IDs as idempotency keys and store processed event IDs in a database with a unique constraint.
5. Over-Engineering Before Necessary
Starting with Event Sourcing, CQRS, and saga orchestrators for a simple application adds enormous complexity. Begin with synchronous calls and a simple message Queue, then introduce patterns like sagas only when you have a demonstrated need for distributed transactions.
6. Ignoring Observability
Without distributed tracing, correlating a request across 5 services is nearly impossible. Every service must propagate trace IDs in headers, log with structured formats, and export metrics to a centralized monitoring system.
Practice Questions
1. What is the difference between choreography and Orchestration in sagas?
Choreography: each service publishes events and listens for events from other services — no central coordinator. Orchestration: a central orchestrator tells each service what to do and manages compensation. Orchestration is easier to manage for complex workflows; choreography is simpler for simple event chains.
2. How does a Message Broker decouple Microservices?
The producer publishes events without knowing which consumers exist. The broker stores events until consumers are ready. A producer failure does not affect consumers and vice versa. New consumers can subscribe without changing producers.
3. What is the Circuit Breaker Pattern and when should you use it?
A circuit breaker monitors failure rates to a downstream service. When failures exceed a threshold, the circuit opens and subsequent calls fail immediately without attempting the call. After a timeout, it allows a test request. Use it for all inter-service HTTP calls to prevent cascading failures.
4. How does eventual consistency affect API design?
APIs must not guarantee immediate consistency across services. A GET /orders/1 response may show "processing" status while the payment and inventory events are still being processed. Use webhook callbacks or polling for clients that need confirmation of completion.
Challenge
Design an Event-Driven Architecture for a ride-sharing platform with services for: (1) Rider Service (requests rides, manages profiles), (2) Driver Service (tracks location, manages availability), (3) Trip Service (matches riders to drivers, tracks trips), (4) Payment Service (processes fares, handles disputes), and (5) Notification Service (sends push/SMS/email). Define the events, Message Broker topology (exchanges, queues, routing keys), and saga for the "Request Ride" flow that compensates if driver cancels or payment fails.
Mini Project: Event-Driven Order Processing
// event-bus.js
// Simple in-Process event bus for microservice simulation
const EventEmitter = require('events');
class EventBus extends EventEmitter {
constructor() {
super();
this.setMaxListeners(50);
this.eventLog = [];
}
publish(eventType, data) {
const event = {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
type: eventType,
timestamp: new Date().toISOString(),
data,
};
this.eventLog.push(event);
console.log(`[EventBus] Publishing: ${eventType} (${event.id})`);
this.emit(eventType, event);
}
subscribe(eventType, handler) {
this.on(eventType, (event) => {
console.log(`[EventBus] Delivering: ${event.type} to handler`);
try {
handler(event);
} catch (err) {
console.error(`[EventBus] Handler error: ${err.message}`);
}
});
}
getHistory(eventType) {
return this.eventLog.filter(e => !eventType || e.type === eventType);
}
}
// ── Usage ──
const bus = new EventBus();
// Order Service publishes events
bus.subscribe('order.placed', (event) => {
console.log(`[Order Service] Order ${event.data.id} placed, publishing event`);
bus.publish('inventory.reserve', { orderId: event.data.id, productId: event.data.productId, qty: event.data.qty });
});
// Inventory Service handles reservation
bus.subscribe('inventory.reserve', (event) => {
console.log(`[Inventory Service] Reserving ${event.data.qty} units of ${event.data.productId}`);
bus.publish('inventory.reserved', { orderId: event.data.orderId, success: true });
});
// Payment Service processes payment after inventory
bus.subscribe('inventory.reserved', (event) => {
console.log(`[Payment Service] Processing payment for order ${event.data.orderId}`);
bus.publish('payment.processed', { orderId: event.data.orderId, amount: 49.99 });
});
// Simulate order placement
bus.publish('order.placed', { id: 'ORD-001', productId: 'PROD-100', qty: 2 });
console.log(`\nTotal events published: ${bus.eventLog.length}`);
Expected output:
[EventBus] Publishing: order.placed (1712345678900-abc123)
[EventBus] Delivering: order.placed to handler
[Order Service] Order ORD-001 placed, publishing event
[EventBus] Publishing: inventory.reserve (1712345678900-def456)
[EventBus] Delivering: inventory.reserve to handler
[Inventory Service] Reserving 2 units of PROD-100
[EventBus] Publishing: inventory.reserved (1712345678900-ghi789)
[EventBus] Delivering: inventory.reserved to handler
[Payment Service] Processing payment for order ORD-001
[EventBus] Publishing: payment.processed (...
Total events published: 4
Congratulations on completing this Microservices communication tutorial! Next, explore message Queue patterns for deeper dive into RabbitMQ and Kafka, then learn about API Gateway patterns for routing requests across Microservices.
- Practice daily — Convert a monolithic endpoint into two services communicating via a Message Broker
- Build a project — Build a three-service order system (orders, inventory, payment) with event-driven communication and a saga for failure handling
- Explore related topics — Check out Apache Kafka for Event Sourcing, gRPC for high-performance inter-service calls, and Kubernetes for service discovery
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro