ScaledByDesign/Insights
ServicesPricingAboutContact
Book a Call
Scaled By Design

Fractional CTO + execution partner for revenue-critical systems.

Company

  • About
  • Services
  • Contact

Resources

  • Insights
  • Pricing
  • FAQ

Legal

  • Privacy Policy
  • Terms of Service

© 2026 ScaledByDesign. All rights reserved.

contact@scaledbydesign.com

On This Page

The Distributed MonolithSynchronous Communication (HTTP/gRPC)When sync works:Making sync calls resilient:Asynchronous Communication (Message Queues)When async works:The Saga Pattern (Multi-Service Transactions)Choosing the Right PatternThe Rules
  1. Insights
  2. Architecture
  3. Microservices Communication Patterns — Sync, Async, and When to Use Each

Microservices Communication Patterns — Sync, Async, and When to Use Each

May 4, 2026·ScaledByDesign·
microservicesarchitecturemessagingapi-designdistributed-systems

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.

Previous
A/B Testing: Server-Side vs. Client-Side — The Technical Trade-offs
Insights
Microservices Communication Patterns — Sync, Async, and When to Use EachMonorepo vs. Polyrepo — The Decision Framework Nobody Gives YouAPI Versioning Strategies That Don't Become a Maintenance NightmareEvent-Driven Architecture Without the PhD — A Practical GuideCQRS Without the Complexity — A Practical Implementation GuideThe Strangler Fig Migration That Saved a 10-Year-Old MonolithWhy You Should Start With a MonolithEvent-Driven Architecture for the Rest of UsThe Real Cost of Microservices at Your ScaleThe Caching Strategy That Cut Our Client's AWS Bill by 60%API Design Mistakes That Will Haunt You for YearsMulti-Tenant Architecture: The Decisions You Can't UndoCI/CD Pipelines That Actually Make You FasterThe Rate Limiting Strategy That Saved Our Client's APIWhen to Rewrite vs Refactor: The Decision Framework

Ready to Ship?

Let's talk about your engineering challenges and how we can help.

Book a Call