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

API GatewayAuthServiceUserServiceOrderServicePaymentServiceMessage Bus / Events

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.

WebMobileIoTGatewayRate LimitAuthLoad BalanceTransformCacheService AService BService CDB / Cache

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.

Download the Checklist
// 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
TriggerwebhookIfSend EmailSMTPLog EventdatabaseUpdate CRMAPI callDonetruefalse

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.

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

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.

47+ companies trusted us
99.99% uptime
< 48hr response

No spam. No contracts. Just a free demo.