← All articlesPlatform Engineering

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,...

Y
Yash Pritwani
15 min read

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.

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 220" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="220" rx="12" fill="#1a1a2e"/><rect x="230" y="15" width="140" height="35" rx="8" fill="#6366f1" opacity="0.9"/><text x="300" y="38" text-anchor="middle" fill="#ffffff" font-size="12" font-family="system-ui" font-weight="bold">API Gateway</text><rect x="30" y="80" width="100" height="50" rx="8" fill="#3b82f6" opacity="0.8"/><text x="80" y="100" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Auth</text><text x="80" y="115" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Service</text><rect x="160" y="80" width="100" height="50" rx="8" fill="#a855f7" opacity="0.8"/><text x="210" y="100" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">User</text><text x="210" y="115" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Service</text><rect x="290" y="80" width="100" height="50" rx="8" fill="#2dd4bf" opacity="0.8"/><text x="340" y="100" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Order</text><text x="340" y="115" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service</text><rect x="420" y="80" width="100" height="50" rx="8" fill="#f59e0b" opacity="0.8"/><text x="470" y="100" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Payment</text><text x="470" y="115" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service</text><line x1="265" y1="50" x2="80" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><line x1="285" y1="50" x2="210" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><line x1="315" y1="50" x2="340" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><line x1="335" y1="50" x2="470" y2="78" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/><ellipse cx="80" cy="175" rx="35" ry="12" fill="none" stroke="#3b82f6" stroke-width="1.5"/><line x1="45" y1="175" x2="45" y2="190" stroke="#3b82f6" stroke-width="1.5"/><line x1="115" y1="175" x2="115" y2="190" stroke="#3b82f6" stroke-width="1.5"/><ellipse cx="80" cy="190" rx="35" ry="12" fill="none" stroke="#3b82f6" stroke-width="1.5"/><line x1="80" y1="130" x2="80" y2="163" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><ellipse cx="340" cy="175" rx="35" ry="12" fill="none" stroke="#2dd4bf" stroke-width="1.5"/><line x1="305" y1="175" x2="305" y2="190" stroke="#2dd4bf" stroke-width="1.5"/><line x1="375" y1="175" x2="375" y2="190" stroke="#2dd4bf" stroke-width="1.5"/><ellipse cx="340" cy="190" rx="35" ry="12" fill="none" stroke="#2dd4bf" stroke-width="1.5"/><line x1="340" y1="130" x2="340" y2="163" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><rect x="155" y="160" width="150" height="30" rx="6" fill="#a855f7" opacity="0.3"/><text x="230" y="180" text-anchor="middle" fill="#a855f7" font-size="10" font-family="system-ui">Message Bus / Events</text><line x1="210" y1="130" x2="210" y2="158" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><line x1="470" y1="130" x2="470" y2="175" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/><line x1="305" y1="175" x2="470" y2="175" stroke="#94a3b8" stroke-width="0.5" stroke-dasharray="3,3" opacity="0.3"/></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Microservices architecture: independent services communicate through an API gateway and event bus.</p></div>

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).

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.

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 180" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="180" rx="12" fill="#1a1a2e"/><rect x="20" y="20" width="70" height="35" rx="6" fill="#3b82f6" opacity="0.8"/><text x="55" y="42" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Web</text><rect x="20" y="65" width="70" height="35" rx="6" fill="#3b82f6" opacity="0.8"/><text x="55" y="87" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Mobile</text><rect x="20" y="110" width="70" height="35" rx="6" fill="#3b82f6" opacity="0.8"/><text x="55" y="132" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">IoT</text><rect x="150" y="20" width="120" height="130" rx="10" fill="#6366f1" opacity="0.9"/><text x="210" y="50" text-anchor="middle" fill="#ffffff" font-size="12" font-family="system-ui" font-weight="bold">Gateway</text><line x1="165" y1="60" x2="255" y2="60" stroke="#ffffff" stroke-width="0.5" opacity="0.3"/><text x="210" y="80" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Rate Limit</text><text x="210" y="95" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Auth</text><text x="210" y="110" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Load Balance</text><text x="210" y="125" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Transform</text><text x="210" y="140" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">Cache</text><rect x="340" y="15" width="95" height="35" rx="6" fill="#a855f7" opacity="0.8"/><text x="387" y="37" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Service A</text><rect x="340" y="60" width="95" height="35" rx="6" fill="#2dd4bf" opacity="0.8"/><text x="387" y="82" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service B</text><rect x="340" y="105" width="95" height="35" rx="6" fill="#f59e0b" opacity="0.8"/><text x="387" y="127" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Service C</text><rect x="490" y="55" width="80" height="45" rx="6" fill="none" stroke="#e2e8f0" stroke-width="1"/><text x="530" y="82" text-anchor="middle" fill="#e2e8f0" font-size="10" font-family="system-ui">DB / Cache</text><defs><marker id="arrow7" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><line x1="92" y1="37" x2="148" y2="55" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="92" y1="82" x2="148" y2="85" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="92" y1="127" x2="148" y2="115" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="272" y1="55" x2="338" y2="32" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="272" y1="85" x2="338" y2="77" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="272" y1="115" x2="338" y2="122" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/><line x1="437" y1="77" x2="488" y2="77" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow7)"/></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">API gateway pattern: a single entry point handles auth, rate limiting, and routing to backend services.</p></div>

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):

// 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

<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 170" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="170" rx="12" fill="#1a1a2e"/><circle cx="60" cy="85" r="25" fill="#f59e0b" opacity="0.85"/><text x="60" y="82" text-anchor="middle" fill="#1a1a2e" font-size="9" font-family="system-ui" font-weight="bold">Trigger</text><text x="60" y="94" text-anchor="middle" fill="#1a1a2e" font-size="8" font-family="system-ui">webhook</text><polygon points="175,55 210,85 175,115 140,85" fill="#6366f1" opacity="0.85"/><text x="175" y="88" text-anchor="middle" fill="#ffffff" font-size="9" font-family="system-ui">If</text><rect x="250" y="35" width="100" height="40" rx="6" fill="#2dd4bf" opacity="0.85"/><text x="300" y="55" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">Send Email</text><text x="300" y="67" text-anchor="middle" fill="#1a1a2e" font-size="8" font-family="system-ui">SMTP</text><rect x="250" y="95" width="100" height="40" rx="6" fill="#a855f7" opacity="0.85"/><text x="300" y="115" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Log Event</text><text x="300" y="127" text-anchor="middle" fill="#ffffff" font-size="8" font-family="system-ui">database</text><rect x="400" y="55" width="100" height="40" rx="6" fill="#3b82f6" opacity="0.85"/><text x="450" y="75" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Update CRM</text><text x="450" y="87" text-anchor="middle" fill="#ffffff" font-size="8" font-family="system-ui">API call</text><circle cx="545" cy="75" r="18" fill="none" stroke="#2dd4bf" stroke-width="2"/><text x="545" y="79" text-anchor="middle" fill="#2dd4bf" font-size="9" font-family="system-ui">Done</text><defs><marker id="arrow10" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><line x1="87" y1="85" x2="138" y2="85" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="210" y1="72" x2="248" y2="55" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="210" y1="98" x2="248" y2="115" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="352" y1="55" x2="398" y2="68" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="352" y1="115" x2="398" y2="82" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><line x1="502" y1="75" x2="525" y2="75" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow10)"/><text x="225" y="45" text-anchor="middle" fill="#2dd4bf" font-size="8" font-family="system-ui">true</text><text x="225" y="120" text-anchor="middle" fill="#a855f7" font-size="8" font-family="system-ui">false</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Workflow automation: triggers, conditions, and actions chain together to eliminate manual processes.</p></div>

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.

#microservices#messaging#event-driven#saga#cqrs

Need help with platform engineering?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.