Microservices Communication Patterns

How to choose the right communication pattern between services without overcomplicating the platform.

Baikal Signal
Choosing the right coupling level before service sprawl chooses for you.

Communication between microservices is critical for system reliability. This guide compares synchronous and asynchronous patterns with practical examples.

Synchronous Communication

Request-response pattern where the caller waits for a response. Simple but creates tight coupling.

Advantages

  • Simple mental model
  • Immediate feedback
  • Easy to debug

Disadvantages

  • Caller blocked while waiting
  • Cascading failures
  • Requires availability of both services

REST APIs

HTTP-based APIs are the most common synchronous pattern.

// User service calls order service
                                                                        const response = await fetch('http://order-service/api/orders', {
                                                                          method: 'POST',
                                                                          headers: { 'Content-Type': 'application/json' },
                                                                          body: JSON.stringify({ userId: 123, items: [...] })
                                                                        });
                                                                        
                                                                        const order = await response.json();

Best Practices

  • Use circuit breakers to prevent cascading failures
  • Implement timeouts (default: 30s is too long)
  • Add retries with exponential backoff
  • Use service mesh for resilience patterns

gRPC

High-performance RPC framework using Protocol Buffers.

// Define service in .proto file
                                                                        service OrderService {
                                                                          rpc CreateOrder (CreateOrderRequest) returns (Order);
                                                                          rpc GetOrder (GetOrderRequest) returns (Order);
                                                                        }
                                                                        
                                                                        message CreateOrderRequest {
                                                                          int32 user_id = 1;
                                                                          repeated OrderItem items = 2;
                                                                        }

When to Use gRPC

  • Internal service-to-service communication
  • Performance-critical paths
  • Strongly-typed contracts
  • Streaming requirements

Performance Comparison

// REST JSON payload: ~500 bytes
                                                                        {
                                                                          "userId": 123,
                                                                          "items": [{"productId": 456, "quantity": 2}]
                                                                        }
                                                                        
                                                                        // gRPC protobuf: ~50 bytes (10x smaller)
                                                                        // Latency: REST ~15ms, gRPC ~3ms (5x faster)

Asynchronous Messaging

Decouple services using message queues or event streams.

Message Queue Pattern

// Publisher
                                                                        await queue.publish('order.created', {
                                                                          orderId: 123,
                                                                          userId: 456,
                                                                          total: 99.99
                                                                        });
                                                                        
                                                                        // Consumer
                                                                        queue.subscribe('order.created', async (message) => {
                                                                          await sendConfirmationEmail(message.userId);
                                                                          await updateInventory(message.orderId);
                                                                        });

Event Stream Pattern

// Using Kafka
                                                                        await producer.send({
                                                                          topic: 'orders',
                                                                          messages: [{
                                                                            key: userId.toString(),
                                                                            value: JSON.stringify(orderEvent)
                                                                          }]
                                                                        });
                                                                        
                                                                        // Multiple consumers can process independently
                                                                        consumer.subscribe({ topic: 'orders' });
                                                                        await consumer.run({
                                                                          eachMessage: async ({ message }) => {
                                                                            // Analytics service
                                                                            await trackMetrics(message.value);
                                                                          }
                                                                        });

Advantages

  • Temporal decoupling (services don't need to be online simultaneously)
  • Better failure isolation
  • Easy to add new consumers
  • Natural fit for event-driven architectures

Disadvantages

  • Eventual consistency
  • More complex debugging
  • Message ordering challenges
  • Need to handle duplicate messages (idempotency)

Choosing the Right Pattern

Use Case Recommendation
User-facing requests REST or gRPC (synchronous)
Background processing Message queue
Analytics/logging Event stream (Kafka)
High-throughput internal APIs gRPC
External integrations REST with webhooks

Summary

Use synchronous communication (REST/gRPC) for user-facing requests requiring immediate responses. Choose asynchronous messaging for background tasks and event notifications. Most systems benefit from a hybrid approach: synchronous for critical path, asynchronous for everything else. Always implement resilience patterns regardless of choice.