Microservices Communication Patterns — Sync, Async, and When to Use Each
The Distributed Monolith
A client decomposed their monolith into 12 microservices. Every service called 3-4 other services synchronously via REST. A single user request triggered a chain of 8 HTTP calls. When one service was slow, every downstream service was slow. When one service went down, the entire system went down.
They'd built a distributed monolith — all the complexity of microservices with none of the benefits.
Synchronous Communication (HTTP/gRPC)
Synchronous calls are the simplest pattern: Service A calls Service B and waits for a response.
// Sync REST call — Service A calls Service B
async function getOrderWithCustomer(orderId: string) {
const order = await db.orders.findUnique({ where: { id: orderId } });
// Synchronous call to customer service — blocks until response
const customer = await fetch(`http://customer-service/api/customers/${order.customerId}`)
.then(r => r.json());
return { ...order, customer };
}When sync works:
✓ Request-response patterns (user needs data back immediately)
✓ Simple queries across services (get customer for order)
✓ Low-latency requirements (< 100ms total)
✓ Few hops (service A → service B, not A → B → C → D)
✗ When the downstream service can be slow or unreliable
✗ When you're chaining 3+ services (latency compounds)
✗ When failure in one service shouldn't affect the caller
✗ For fire-and-forget operations (send email, log event)
Making sync calls resilient:
// Circuit breaker pattern — stop calling a failing service
import CircuitBreaker from "opossum";
const customerServiceBreaker = new CircuitBreaker(
async (customerId: string) => {
return fetch(`http://customer-service/api/customers/${customerId}`)
.then(r => r.json());
},
{
timeout: 3000, // Fail if no response in 3s
errorThresholdPercentage: 50, // Open circuit at 50% error rate
resetTimeout: 10000, // Try again after 10s
}
);
// Fallback when circuit is open
customerServiceBreaker.fallback((customerId) => ({
id: customerId,
name: "Customer", // Degraded response
_fallback: true,
}));
const customer = await customerServiceBreaker.fire(order.customerId);Asynchronous Communication (Message Queues)
Services communicate through a message broker. The sender doesn't wait for a response:
// Async: Order service publishes event, doesn't wait
async function placeOrder(orderData: CreateOrderInput) {
const order = await db.orders.create({ data: orderData });
// Publish event — returns immediately
await messageQueue.publish("order.placed", {
orderId: order.id,
customerId: order.customerId,
items: order.items,
total: order.total,
});
return order; // Response doesn't depend on downstream services
}
// Email service subscribes and processes independently
messageQueue.subscribe("order.placed", async (event) => {
await sendOrderConfirmationEmail(event.customerId, event.orderId);
});
// Inventory service subscribes and processes independently
messageQueue.subscribe("order.placed", async (event) => {
await reserveInventory(event.items);
});When async works:
✓ Fire-and-forget operations (send email, update analytics)
✓ Long-running processes (generate report, process image)
✓ Fan-out patterns (one event triggers multiple handlers)
✓ Decoupling services (sender doesn't know about receivers)
✓ Handling traffic spikes (queue absorbs burst, consumers process at their pace)
✗ When the caller needs an immediate response from the consumer
✗ For simple request-response queries
✗ When ordering guarantees are critical (need careful design)
The Saga Pattern (Multi-Service Transactions)
When a business operation spans multiple services, you can't use a database transaction. Use a saga instead:
// Choreography saga: each service reacts to events
// Order placed → Reserve inventory → Charge payment → Confirm order
// Step 1: Order service
messageQueue.subscribe("checkout.initiated", async (event) => {
const order = await createOrder(event);
await messageQueue.publish("order.created", { orderId: order.id, items: event.items });
});
// Step 2: Inventory service
messageQueue.subscribe("order.created", async (event) => {
try {
await reserveInventory(event.items);
await messageQueue.publish("inventory.reserved", { orderId: event.orderId });
} catch (error) {
await messageQueue.publish("inventory.failed", { orderId: event.orderId, reason: error.message });
}
});
// Step 3: Payment service
messageQueue.subscribe("inventory.reserved", async (event) => {
try {
await chargePayment(event.orderId);
await messageQueue.publish("payment.completed", { orderId: event.orderId });
} catch (error) {
await messageQueue.publish("payment.failed", { orderId: event.orderId });
}
});
// Compensation: if payment fails, release inventory
messageQueue.subscribe("payment.failed", async (event) => {
await releaseInventory(event.orderId);
await cancelOrder(event.orderId);
});Choosing the Right Pattern
| Scenario | Pattern | Why |
|-----------------------------------|----------------|-------------------------------|
| Get user profile for display | Sync (REST) | Need immediate response |
| Send order confirmation email | Async (queue) | Fire and forget |
| Process payment after order | Saga | Multi-service transaction |
| Real-time search | Sync (gRPC) | Low latency required |
| Generate monthly report | Async (queue) | Long-running, no rush |
| Update inventory after order | Async (event) | Decouple order from inventory |
| Get product recommendations | Sync + cache | Need response, cacheable |
The Rules
Rule 1: Default to async. Use sync only when you need an immediate response.
Rule 2: Never chain more than 2 sync calls. If you need A→B→C, redesign.
Rule 3: Every sync call needs a circuit breaker and timeout.
Rule 4: Every async consumer must be idempotent (safe to process twice).
Rule 5: Use dead letter queues for failed messages (don't lose data).
Rule 6: If two services always deploy together, they shouldn't be separate services.
The communication pattern between services matters more than the services themselves. Get it wrong and you have a distributed monolith that's slower, more complex, and harder to debug than the monolith you started with.