Table of Contents
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.