Difficulty: Advanced
How do microservices communicate in a Node.js architecture? Compare synchronous and asynchronous patterns.
Microservices communicate through two main patterns: synchronous (request-response) and asynchronous (event-driven/messaging). Each has different tradeoffs for latency, coupling, reliability, and complexity.
Synchronous communication uses HTTP/REST or gRPC. The caller waits for a response. This is simple to implement and reason about, but creates tight coupling and cascading failures. If Service B is down, Service A's request fails immediately.
Asynchronous communication uses message queues (RabbitMQ, AWS SQS) or event streaming (Kafka, Redis Pub/Sub). The sender publishes a message and moves on without waiting. This provides loose coupling, better fault tolerance, and natural load leveling, but adds complexity in debugging and ensuring eventual consistency.
In practice, most architectures use both patterns. Synchronous for queries that need immediate responses (GET user profile). Asynchronous for operations that can be processed later (send email, update search index, generate reports).
// Order Service calls User Service via HTTP
async function getUserForOrder(userId) {
try {
const response = await fetch(
`http://user-service:3001/api/users/${userId}`,
{
headers: { 'X-Service': 'order-service' },
signal: AbortSignal.timeout(5000), // 5s timeout
}
);
if (!response.ok) {
throw new Error(`User service returned ${response.status}`);
}
return await response.json();
} catch (err) {
if (err.name === 'TimeoutError') {
console.error('User service timeout');
}
throw err;
}
}
// Circuit breaker pattern
class CircuitBreaker {
constructor(fn, { threshold = 5, timeout = 30000 } = {}) {
this.fn = fn;
this.failures = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextRetry = 0;
}
async call(...args) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextRetry) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await this.fn(...args);
this.failures = 0;
this.state = 'CLOSED';
return result;
} catch (err) {
this.failures++;
if (this.failures >= this.threshold) {
this.state = 'OPEN';
this.nextRetry = Date.now() + this.timeout;
}
throw err;
}
}
}
const userService = new CircuitBreaker(getUserForOrder);
HTTP between services is simple but creates coupling. Circuit breakers prevent cascading failures by stopping calls to failing services.
const amqp = require('amqplib');
// Publisher (Order Service)
async function publishOrderCreated(order) {
const connection = await amqp.connect(process.env.RABBITMQ_URL);
const channel = await connection.createChannel();
const exchange = 'orders';
await channel.assertExchange(exchange, 'topic', { durable: true });
channel.publish(
exchange,
'order.created',
Buffer.from(JSON.stringify(order)),
{ persistent: true } // Survive broker restart
);
console.log('Published order.created event');
}
// Consumer (Email Service)
async function startEmailConsumer() {
const connection = await amqp.connect(process.env.RABBITMQ_URL);
const channel = await connection.createChannel();
const exchange = 'orders';
const queue = 'email-notifications';
await channel.assertExchange(exchange, 'topic', { durable: true });
await channel.assertQueue(queue, { durable: true });
await channel.bindQueue(queue, exchange, 'order.*');
// Prefetch: process one message at a time
channel.prefetch(1);
channel.consume(queue, async (msg) => {
try {
const order = JSON.parse(msg.content.toString());
await sendOrderConfirmationEmail(order);
channel.ack(msg); // Acknowledge success
} catch (err) {
console.error('Failed to process:', err);
channel.nack(msg, false, true); // Requeue
}
});
}
startEmailConsumer();
RabbitMQ decouples services. The order service publishes events. The email service consumes them independently. Messages persist if the consumer is down.
const Redis = require('ioredis');
// Publisher
const publisher = new Redis(process.env.REDIS_URL);
async function emitEvent(channel, data) {
await publisher.publish(channel, JSON.stringify({
...data,
timestamp: Date.now(),
source: 'order-service',
}));
}
// Emit from order service
await emitEvent('order:created', { orderId: 123, userId: 456 });
await emitEvent('order:shipped', { orderId: 123, trackingId: 'TRK001' });
// Subscriber (Notification Service)
const subscriber = new Redis(process.env.REDIS_URL);
subscriber.subscribe('order:created', 'order:shipped', (err, count) => {
console.log(`Subscribed to ${count} channels`);
});
subscriber.on('message', (channel, message) => {
const data = JSON.parse(message);
console.log(`[${channel}]`, data);
switch (channel) {
case 'order:created':
sendPushNotification(data.userId, 'Order confirmed!');
break;
case 'order:shipped':
sendPushNotification(data.userId, `Shipped: ${data.trackingId}`);
break;
}
});
// Pattern subscription
subscriber.psubscribe('order:*'); // Match all order events
Redis Pub/Sub is fire-and-forget (no persistence). Good for real-time notifications. For reliability, use RabbitMQ or Kafka instead.
REST, gRPC, Message Queue, Event-Driven, Service Discovery