Microservices Communication Patterns: Sync, Async, and Event-Driven
Master the communication patterns between microservices. Synchronous REST/gRPC, asynchronous messaging with RabbitMQ/NATS, event sourcing, saga pattern,...
The Communication Challenge
When you split a monolith into microservices, the biggest challenge is how services communicate. The wrong pattern leads to distributed monoliths, cascading failures, and debugging nightmares.
Microservices architecture: independent services communicate through an API gateway and event bus.
Pattern 1: Synchronous Request-Response
The simplest pattern. Service A calls Service B and waits for a response.
Order Service --HTTP/gRPC--> Payment Service
| |
|<-------- Response ---------|
|
|--HTTP/gRPC--> Inventory Service
| |
|<-------- Response ---------|
// Order Service calling Payment Service
async function createOrder(orderData: OrderInput): Promise<Order> {
// Call payment service (synchronous)
const paymentResult = await fetch('http://payment-service:3000/api/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: orderData.total,
currency: 'USD',
customerId: orderData.customerId,
}),
signal: AbortSignal.timeout(5000), // 5 second timeout
});
if (!paymentResult.ok) {
throw new PaymentFailedError('Payment declined');
}
// Call inventory service (synchronous)
const inventoryResult = await fetch('http://inventory-service:3000/api/reserve', {
method: 'POST',
body: JSON.stringify({ items: orderData.items }),
signal: AbortSignal.timeout(5000),
});
if (!inventoryResult.ok) {
// Compensate: refund the payment
await fetch('http://payment-service:3000/api/refund', {
method: 'POST',
body: JSON.stringify({ paymentId: paymentResult.id }),
});
throw new InventoryError('Items not available');
}
return createOrderRecord(orderData, paymentResult, inventoryResult);
}
When to use: Simple CRUD operations, queries that need immediate results, low-latency requirements.
Problems: Cascading failures (if Payment Service is down, Order Service fails), temporal coupling (both services must be running), increased latency (sequential calls).
Get more insights on Platform Engineering
Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.
Pattern 2: Asynchronous Messaging
Services communicate via a message broker. The sender does not wait for a response.
Order Service --publish--> [Message Broker] --consume--> Payment Service
--consume--> Inventory Service
--consume--> Notification Service
// Order Service publishes event (NATS example)
import { connect, JSONCodec } from 'nats';
const nc = await connect({ servers: 'nats://nats:4222' });
const codec = JSONCodec();
async function createOrder(orderData: OrderInput): Promise<Order> {
// Save order with 'pending' status
const order = await db.orders.create({
...orderData,
status: 'pending',
});
// Publish event (fire and forget)
nc.publish('orders.created', codec.encode({
orderId: order.id,
customerId: order.customerId,
items: order.items,
total: order.total,
timestamp: new Date().toISOString(),
}));
return order; // Return immediately, processing happens async
}
// Payment Service subscribes to events
const sub = nc.subscribe('orders.created');
for await (const msg of sub) {
const order = codec.decode(msg.data);
try {
const payment = await processPayment(order);
// Publish payment result
nc.publish('payments.completed', codec.encode({
orderId: order.orderId,
paymentId: payment.id,
status: 'success',
}));
} catch (error) {
nc.publish('payments.failed', codec.encode({
orderId: order.orderId,
reason: error.message,
}));
}
}
When to use: When services do not need immediate responses, fire-and-forget notifications, high-throughput processing, decoupled systems.
API gateway pattern: a single entry point handles auth, rate limiting, and routing to backend services.
Pattern 3: Event-Driven with Event Store
Events are the source of truth. Services react to events and maintain their own state.
[Event Store]
orders.created → Payment Service (charge customer)
payments.completed → Inventory Service (reserve items)
inventory.reserved → Shipping Service (create shipment)
shipment.created → Notification Service (email customer)
// Event store with NATS JetStream
import { connect, AckPolicy, DeliverPolicy } from 'nats';
const nc = await connect({ servers: 'nats://nats:4222' });
const js = nc.jetstream();
const jsm = await nc.jetstreamManager();
// Create stream (durable event store)
await jsm.streams.add({
name: 'ORDERS',
subjects: ['orders.>'],
retention: 'limits',
max_age: 30 * 24 * 60 * 60 * 1e9, // 30 days in nanoseconds
storage: 'file',
});
// Publish event
await js.publish('orders.created', codec.encode(orderEvent));
// Consume with durable consumer (survives restarts)
const consumer = await js.consumers.get('ORDERS', 'payment-service');
const messages = await consumer.consume();
for await (const msg of messages) {
const event = codec.decode(msg.data);
await processEvent(event);
msg.ack(); // Acknowledge after processing
}
Pattern 4: Saga Pattern
For distributed transactions across services, use the Saga pattern. Each step has a compensating action for rollback.
Create Order → Charge Payment → Reserve Inventory → Create Shipment
↓ ↓ ↓ ↓
Cancel Order ← Refund Payment ← Release Inventory (compensating actions)
Choreography-based saga (events):
// Each service listens and reacts
// Payment Service
on('orders.created', async (order) => {
try {
await chargePayment(order);
emit('payments.completed', { orderId: order.id });
} catch {
emit('payments.failed', { orderId: order.id });
}
});
// Inventory Service
on('payments.completed', async (event) => {
try {
await reserveItems(event.orderId);
emit('inventory.reserved', { orderId: event.orderId });
} catch {
emit('inventory.failed', { orderId: event.orderId });
// Payment service listens and refunds
}
});
// Payment Service (compensating)
on('inventory.failed', async (event) => {
await refundPayment(event.orderId);
emit('payments.refunded', { orderId: event.orderId });
});
Orchestration-based saga (central coordinator):
Free Resource
Free Cloud Architecture Checklist
A 47-point checklist covering security, scalability, cost optimization, and disaster recovery for production cloud environments.
// Saga orchestrator
class OrderSaga {
async execute(orderData: OrderInput): Promise<string> {
const steps: SagaStep[] = [
{
action: () => paymentService.charge(orderData),
compensate: (result) => paymentService.refund(result.paymentId),
},
{
action: () => inventoryService.reserve(orderData.items),
compensate: (result) => inventoryService.release(result.reservationId),
},
{
action: () => shippingService.createShipment(orderData),
compensate: (result) => shippingService.cancel(result.shipmentId),
},
];
const results: any[] = [];
for (let i = 0; i < steps.length; i++) {
try {
results.push(await steps[i].action());
} catch (error) {
// Compensate all previous steps in reverse
for (let j = i - 1; j >= 0; j--) {
await steps[j].compensate(results[j]);
}
throw new SagaFailedError(error.message);
}
}
return 'Order completed successfully';
}
}
Pattern Comparison
| Pattern | Coupling | Complexity | Latency | Data Consistency |
|---|---|---|---|---|
| Sync request-response | High | Low | Low (if all up) | Strong |
| Async messaging | Low | Medium | Higher | Eventual |
| Event-driven | Very low | High | Higher | Eventual |
| Saga (choreography) | Low | High | Higher | Eventual |
| Saga (orchestration) | Medium | Medium | Higher | Eventual |
Workflow automation: triggers, conditions, and actions chain together to eliminate manual processes.
Choosing the Right Pattern
Does the caller need an immediate response?
├── Yes → Sync (REST/gRPC)
│ └── Can the caller tolerate failure?
│ ├── Yes → Add circuit breaker + retry
│ └── No → Consider async with response queue
└── No → Async messaging
└── Do you need ordering guarantees?
├── Yes → Event streaming (NATS JetStream, Kafka)
└── No → Simple pub/sub (NATS core, RabbitMQ)
At TechSaaS, our self-hosted Docker services use synchronous HTTP calls (they are on the same Docker network, so latency is microseconds). For client architectures with true microservices, we implement async messaging with NATS for lightweight setups or Kafka for high-throughput needs, and the saga pattern for distributed transactions.
Related Service
Cloud Solutions
Let our experts help you build the right technology strategy for your business.
Need help with platform engineering?
TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.
We Will Build You a Demo Site — For Free
Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.
No spam. No contracts. Just a free demo.