Service Communication
How microservices talk to each other - REST, gRPC, message queues, and when to use each.
7 min read
Communication Patterns#
Services need to communicate. There are two main approaches:
| Pattern | Style | Use When |
|---|---|---|
| Synchronous | Request-Response | Need immediate answer |
| Asynchronous | Fire-and-Forget | Can process later |
Synchronous (REST/gRPC):
Order Service ──── Request ────► User Service
◄─── Response ────
Asynchronous (Message Queue):
Order Service ──── Message ────► Queue ────► Email Service
(doesn't wait)
Option 1: REST APIs (HTTP)#
The most common approach - services expose HTTP endpoints:
Basic Service-to-Service Call#
javascript
// Order Service calling User Service
import axios from 'axios';
const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://localhost:3001';
export async function getUserById(userId) {
try {
const response = await axios.get(`${USER_SERVICE_URL}/api/users/${userId}`);
return response.data.data;
} catch (error) {
if (error.response?.status === 404) {
return null;
}
throw error;
}
}
// Usage in Order Service
router.post('/orders', async (req, res) => {
const { userId, productIds } = req.body;
// Call User Service
const user = await getUserById(userId);
if (!user) {
return res.status(400).json({ error: 'User not found' });
}
// Continue with order creation...
});
Service Client Pattern#
Create reusable clients for each service:
javascript
// src/clients/userClient.js
import axios from 'axios';
const client = axios.create({
baseURL: process.env.USER_SERVICE_URL,
timeout: 5000,
});
export const UserClient = {
async getById(id) {
const { data } = await client.get(`/api/users/${id}`);
return data.data;
},
async getByIds(ids) {
const { data } = await client.post('/api/users/batch', { ids });
return data.data;
},
async validateToken(token) {
const { data } = await client.post('/api/auth/validate', { token });
return data.data;
},
};
// src/clients/productClient.js
export const ProductClient = {
async getById(id) { /* ... */ },
async getByIds(ids) { /* ... */ },
async checkInventory(productId, quantity) { /* ... */ },
};
Pros & Cons of REST#
Pros:
- Simple and familiar
- Easy to debug (HTTP tools, curl)
- Wide language support
- Human-readable (JSON)
Cons:
- Higher latency than gRPC
- No built-in schema validation
- Requires manual retry logic
- HTTP overhead
Option 2: gRPC#
Google's high-performance RPC framework using Protocol Buffers:
Define Service with Protocol Buffers#
protobuf
// user.proto
syntax = "proto3";
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc GetUsers (GetUsersRequest) returns (GetUsersResponse);
rpc ValidateToken (ValidateTokenRequest) returns (ValidateTokenResponse);
}
message GetUserRequest {
string id = 1;
}
message User {
string id = 1;
string email = 2;
string name = 3;
string created_at = 4;
}
message GetUsersRequest {
repeated string ids = 1;
}
message GetUsersResponse {
repeated User users = 1;
}
message ValidateTokenRequest {
string token = 1;
}
message ValidateTokenResponse {
bool valid = 1;
string user_id = 2;
}
gRPC Server (User Service)#
javascript
// user-service/src/grpc-server.js
import grpc from '@grpc/grpc-js';
import protoLoader from '@grpc/proto-loader';
import { UserService } from './services/userService.js';
const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).user;
const server = new grpc.Server();
server.addService(userProto.UserService.service, {
getUser: async (call, callback) => {
try {
const user = await UserService.findById(call.request.id);
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
message: 'User not found',
});
}
callback(null, user);
} catch (error) {
callback({
code: grpc.status.INTERNAL,
message: error.message,
});
}
},
getUsers: async (call, callback) => {
try {
const users = await UserService.findByIds(call.request.ids);
callback(null, { users });
} catch (error) {
callback({
code: grpc.status.INTERNAL,
message: error.message,
});
}
},
});
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
console.log('gRPC server running on port 50051');
});
gRPC Client (Order Service)#
javascript
// order-service/src/clients/userClient.js
import grpc from '@grpc/grpc-js';
import protoLoader from '@grpc/proto-loader';
const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).user;
const client = new userProto.UserService(
process.env.USER_SERVICE_GRPC_URL || 'localhost:50051',
grpc.credentials.createInsecure()
);
export const UserGrpcClient = {
getById(id) {
return new Promise((resolve, reject) => {
client.getUser({ id }, (error, response) => {
if (error) reject(error);
else resolve(response);
});
});
},
getByIds(ids) {
return new Promise((resolve, reject) => {
client.getUsers({ ids }, (error, response) => {
if (error) reject(error);
else resolve(response.users);
});
});
},
};
Pros & Cons of gRPC#
Pros:
- Very fast (binary protocol)
- Strong typing (protobuf schemas)
- Bi-directional streaming
- Code generation for clients
- Built-in retries and deadlines
Cons:
- Learning curve
- Harder to debug (binary)
- Browser support requires proxy
- More setup than REST
Option 3: Message Queues (Async)#
For operations that don't need immediate response:
Popular Options#
| Queue | Best For | Features |
|---|---|---|
| RabbitMQ | General messaging | Routing, reliability |
| Redis Pub/Sub | Simple pub/sub | Fast, lightweight |
| Apache Kafka | Event streaming | High throughput, replay |
| AWS SQS | Cloud, serverless | Managed, scalable |
| BullMQ | Node.js jobs | Redis-based, simple |
RabbitMQ Example#
bash
npm install amqplib
javascript
// Producer (Order Service)
import amqp from 'amqplib';
const RABBITMQ_URL = process.env.RABBITMQ_URL || 'amqp://localhost';
export async function publishOrderCreated(order) {
const connection = await amqp.connect(RABBITMQ_URL);
const channel = await connection.createChannel();
const queue = 'order.created';
await channel.assertQueue(queue, { durable: true });
channel.sendToQueue(queue, Buffer.from(JSON.stringify({
orderId: order.id,
userId: order.userId,
total: order.total,
createdAt: new Date().toISOString(),
})), {
persistent: true,
});
console.log(`Published order.created: ${order.id}`);
await channel.close();
await connection.close();
}
// Usage
router.post('/orders', async (req, res) => {
const order = await OrderService.create(req.body);
// Publish event (async, don't wait)
publishOrderCreated(order).catch(console.error);
res.status(201).json({ data: order });
});
javascript
// Consumer (Email Service)
import amqp from 'amqplib';
import { sendOrderConfirmation } from './services/emailService.js';
async function startConsumer() {
const connection = await amqp.connect(RABBITMQ_URL);
const channel = await connection.createChannel();
const queue = 'order.created';
await channel.assertQueue(queue, { durable: true });
// Process one at a time
channel.prefetch(1);
console.log('Waiting for messages...');
channel.consume(queue, async (msg) => {
const order = JSON.parse(msg.content.toString());
console.log(`Processing order: ${order.orderId}`);
try {
await sendOrderConfirmation(order);
channel.ack(msg); // Mark as processed
} catch (error) {
console.error('Failed to process:', error);
channel.nack(msg, false, true); // Requeue
}
});
}
startConsumer();
BullMQ Example (Simpler for Node.js)#
bash
npm install bullmq
javascript
// Producer
import { Queue } from 'bullmq';
const emailQueue = new Queue('email', {
connection: { host: 'localhost', port: 6379 }
});
export async function queueOrderConfirmation(order) {
await emailQueue.add('order-confirmation', {
orderId: order.id,
userEmail: order.userEmail,
total: order.total,
}, {
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
});
}
javascript
// Consumer (Worker)
import { Worker } from 'bullmq';
import { sendEmail } from './services/emailService.js';
const worker = new Worker('email', async (job) => {
const { orderId, userEmail, total } = job.data;
await sendEmail({
to: userEmail,
subject: `Order #${orderId} Confirmed`,
html: `Your order of $${total} has been confirmed!`,
});
return { success: true };
}, {
connection: { host: 'localhost', port: 6379 }
});
worker.on('completed', (job) => {
console.log(`Job ${job.id} completed`);
});
worker.on('failed', (job, error) => {
console.error(`Job ${job.id} failed:`, error);
});
Pros & Cons of Message Queues#
Pros:
- Decoupled services
- Handles spikes (queue buffers)
- Retry built-in
- Service can be offline
- Better fault tolerance
Cons:
- Eventually consistent
- More infrastructure
- Debugging across async flows
- Message ordering challenges
Choosing Communication Pattern#
| Scenario | Best Choice |
|---|---|
| Need immediate response | REST or gRPC |
| High performance internal | gRPC |
| Background processing | Message Queue |
| Notifications, emails | Message Queue |
| External APIs | REST |
| Event-driven architecture | Message Queue (Kafka) |
Hybrid Approach (Common)#
javascript
// Order Service
router.post('/orders', async (req, res) => {
// Synchronous: Validate user (need answer now)
const user = await UserClient.getById(req.body.userId);
if (!user) return res.status(400).json({ error: 'Invalid user' });
// Synchronous: Check inventory (need answer now)
const available = await ProductClient.checkInventory(req.body.productId, req.body.quantity);
if (!available) return res.status(400).json({ error: 'Out of stock' });
// Create order
const order = await OrderService.create(req.body);
// Asynchronous: Send confirmation email (don't need to wait)
await messageQueue.publish('order.created', order);
// Asynchronous: Update analytics (don't need to wait)
await messageQueue.publish('analytics.order', order);
res.status(201).json({ data: order });
});
Key Takeaways#
- REST for simplicity - Good default for most service communication
- gRPC for performance - When latency matters, internal services
- Message queues for async - Background jobs, events, notifications
- Mix approaches - Use the right tool for each use case
- Retries and timeouts - Always handle network failures
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.