Microservices communicate through various patterns. After building production microservices, here’s when to use synchronous vs asynchronous communication.

Communication Patterns

Synchronous Communication

Characteristics:

  • Request-response pattern
  • Blocking calls
  • Immediate response
  • Tight coupling

Use cases:

  • User-facing requests
  • Real-time operations
  • Simple queries

Asynchronous Communication

Characteristics:

  • Event-driven
  • Non-blocking
  • Eventual consistency
  • Loose coupling

Use cases:

  • Background processing
  • Event notifications
  • Long-running tasks

Synchronous Patterns

REST API

// Client
const response = await fetch('http://user-service/api/users/123');
const user = await response.json();

// Server
app.get('/api/users/:id', async (req, res) => {
    const user = await userService.getUser(req.params.id);
    res.json(user);
});

Pros:

  • Simple to implement
  • Standard HTTP
  • Easy to debug
  • Good tooling

Cons:

  • Tight coupling
  • Cascading failures
  • No guaranteed delivery

gRPC

// Client
const user = await userClient.getUser({ userId: '123' });

// Server
async getUser(call, callback) {
    const user = await userService.getUser(call.request.userId);
    callback(null, user);
}

Pros:

  • High performance
  • Type safety
  • Streaming support
  • Efficient serialization

Cons:

  • More complex
  • Less tooling
  • HTTP/2 required

Asynchronous Patterns

Message Queues

// Producer
await sqs.sendMessage({
    QueueUrl: queueUrl,
    MessageBody: JSON.stringify({
        event: 'user.created',
        userId: '123',
        data: userData
    })
}).promise();

// Consumer
const messages = await sqs.receiveMessage({
    QueueUrl: queueUrl
}).promise();

for (const message of messages.Messages) {
    const event = JSON.parse(message.Body);
    await handleEvent(event);
    await sqs.deleteMessage({
        QueueUrl: queueUrl,
        ReceiptHandle: message.ReceiptHandle
    }).promise();
}

Pros:

  • Decoupled services
  • Guaranteed delivery
  • Scalable
  • Resilient

Cons:

  • Eventual consistency
  • More complex
  • Debugging harder

Event-Driven

// Publisher
await eventBus.publish({
    type: 'OrderCreated',
    orderId: '123',
    userId: '456',
    total: 99.99
});

// Subscriber
eventBus.subscribe('OrderCreated', async (event) => {
    await inventoryService.reserveItems(event.orderId);
    await emailService.sendConfirmation(event.userId);
    await analyticsService.trackOrder(event.orderId);
});

Pros:

  • Loose coupling
  • Scalable
  • Flexible
  • Eventual consistency

Cons:

  • Complex debugging
  • Event ordering
  • Duplicate events

Hybrid Approach

Request-Response + Events

// Synchronous for user request
app.post('/api/orders', async (req, res) => {
    // Create order synchronously
    const order = await orderService.createOrder(req.body);
    
    // Publish event asynchronously
    await eventBus.publish({
        type: 'OrderCreated',
        orderId: order.id
    });
    
    res.json(order);
});

// Asynchronous for background processing
eventBus.subscribe('OrderCreated', async (event) => {
    await inventoryService.reserveItems(event.orderId);
    await emailService.sendConfirmation(event.userId);
});

When to Use Each

Use Synchronous When:

  • User-facing requests - Need immediate response
  • Simple queries - Direct data access
  • Real-time operations - Immediate feedback
  • Strong consistency - ACID transactions

Use Asynchronous When:

  • Background processing - Can be delayed
  • Event notifications - Fire and forget
  • Long-running tasks - Don’t block
  • Eventual consistency - Acceptable

Best Practices

  1. Use REST for APIs - Standard and simple
  2. Use gRPC for internal - High performance
  3. Use queues for async - Reliable processing
  4. Use events for decoupling - Loose coupling
  5. Handle failures - Retries and DLQs
  6. Monitor communication - Track latency/errors
  7. Document contracts - API specifications
  8. Version APIs - Backward compatibility

Conclusion

Choose communication pattern based on:

  • Latency requirements - Sync for immediate
  • Consistency needs - Sync for strong consistency
  • Coupling tolerance - Async for loose coupling
  • Failure handling - Async for resilience

Use synchronous for user-facing, asynchronous for background. The patterns shown here handle production microservices.


Microservices communication patterns from August 2020, covering synchronous and asynchronous approaches.