Synchronous Communication
When services need immediate responses, synchronous communication is the way to go. This chapter covers REST and gRPC - the two dominant patterns.Learning Objectives:
- Design effective REST APIs for microservices
- Implement gRPC for high-performance communication
- Choose between REST and gRPC for different scenarios
- Handle failures in synchronous communication
REST API Design
RESTful Principles for Microservices
Copy
// User Service - REST API Example
const express = require('express');
const router = express.Router();
// Resource-based URLs
// GET /users - List users
// GET /users/:id - Get single user
// POST /users - Create user
// PUT /users/:id - Update user (full)
// PATCH /users/:id - Update user (partial)
// DELETE /users/:id - Delete user
// List users with pagination and filtering
router.get('/users', async (req, res) => {
const { page = 1, limit = 20, status, role } = req.query;
const users = await userService.findAll({
page: parseInt(page),
limit: Math.min(parseInt(limit), 100),
filters: { status, role }
});
res.json({
data: users.items,
pagination: {
page: users.page,
limit: users.limit,
total: users.total,
totalPages: Math.ceil(users.total / users.limit)
},
_links: {
self: `/users?page=${page}&limit=${limit}`,
next: users.hasNext ? `/users?page=${parseInt(page) + 1}&limit=${limit}` : null,
prev: users.hasPrev ? `/users?page=${parseInt(page) - 1}&limit=${limit}` : null
}
});
});
// Get single user
router.get('/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: {
code: 'USER_NOT_FOUND',
message: `User with ID ${req.params.id} not found`
}
});
}
res.json({
data: user,
_links: {
self: `/users/${user.id}`,
orders: `/users/${user.id}/orders`,
preferences: `/users/${user.id}/preferences`
}
});
});
// Create user
router.post('/users', validateBody(createUserSchema), async (req, res) => {
try {
const user = await userService.create(req.body);
res.status(201)
.location(`/users/${user.id}`)
.json({
data: user,
_links: {
self: `/users/${user.id}`
}
});
} catch (error) {
if (error.code === 'DUPLICATE_EMAIL') {
return res.status(409).json({
error: {
code: 'EMAIL_EXISTS',
message: 'A user with this email already exists'
}
});
}
throw error;
}
});
HTTP Status Codes
Copy
// Standard Status Code Usage
const StatusCodes = {
// Success
OK: 200, // GET, PUT, PATCH success
CREATED: 201, // POST success (resource created)
ACCEPTED: 202, // Async operation started
NO_CONTENT: 204, // DELETE success
// Client Errors
BAD_REQUEST: 400, // Invalid input
UNAUTHORIZED: 401, // Not authenticated
FORBIDDEN: 403, // Not authorized
NOT_FOUND: 404, // Resource doesn't exist
METHOD_NOT_ALLOWED: 405,
CONFLICT: 409, // Resource conflict (duplicate)
UNPROCESSABLE_ENTITY: 422, // Validation failed
TOO_MANY_REQUESTS: 429, // Rate limited
// Server Errors
INTERNAL_SERVER_ERROR: 500,
BAD_GATEWAY: 502, // Upstream service failed
SERVICE_UNAVAILABLE: 503, // Service temporarily down
GATEWAY_TIMEOUT: 504 // Upstream timeout
};
// Error Response Format
const errorResponse = (res, statusCode, code, message, details = null) => {
const response = {
error: {
code,
message,
timestamp: new Date().toISOString(),
path: res.req.originalUrl,
requestId: res.req.id
}
};
if (details) {
response.error.details = details;
}
return res.status(statusCode).json(response);
};
// Validation Error Example
router.post('/users', async (req, res) => {
const { error, value } = userSchema.validate(req.body, { abortEarly: false });
if (error) {
return errorResponse(res, 422, 'VALIDATION_ERROR', 'Validation failed',
error.details.map(d => ({
field: d.path.join('.'),
message: d.message
}))
);
}
// ... create user
});
API Versioning
Copy
// URL Path Versioning (Recommended for microservices)
app.use('/api/v1/users', v1UserRoutes);
app.use('/api/v2/users', v2UserRoutes);
// Header Versioning
app.use('/api/users', (req, res, next) => {
const version = req.headers['api-version'] || 'v1';
req.apiVersion = version;
next();
});
// Content Negotiation
app.use('/api/users', (req, res, next) => {
const accept = req.headers['accept'];
// Accept: application/vnd.company.user.v2+json
const match = accept?.match(/application\/vnd\.company\.user\.(v\d+)\+json/);
req.apiVersion = match ? match[1] : 'v1';
next();
});
// Version-Specific Response
const userResponseV1 = (user) => ({
id: user.id,
name: user.name,
email: user.email
});
const userResponseV2 = (user) => ({
id: user.id,
fullName: user.name,
emailAddress: user.email,
createdAt: user.createdAt,
metadata: user.metadata
});
router.get('/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
const formatter = req.apiVersion === 'v2' ? userResponseV2 : userResponseV1;
res.json({ data: formatter(user) });
});
Service-to-Service HTTP Clients
Building a Robust HTTP Client
Copy
const axios = require('axios');
const CircuitBreaker = require('opossum');
class ServiceClient {
constructor(options) {
this.serviceName = options.serviceName;
this.baseURL = options.baseURL;
this.timeout = options.timeout || 5000;
// Create axios instance
this.client = axios.create({
baseURL: this.baseURL,
timeout: this.timeout,
headers: {
'Content-Type': 'application/json'
}
});
// Add request interceptor for tracing
this.client.interceptors.request.use((config) => {
config.headers['X-Request-ID'] = this.generateRequestId();
config.headers['X-Correlation-ID'] = this.getCorrelationId();
config.headers['X-Service-Name'] = process.env.SERVICE_NAME;
config.metadata = { startTime: Date.now() };
return config;
});
// Add response interceptor for logging
this.client.interceptors.response.use(
(response) => {
const duration = Date.now() - response.config.metadata.startTime;
this.logRequest(response.config, response.status, duration);
return response;
},
(error) => {
const duration = Date.now() - error.config?.metadata?.startTime;
this.logRequest(error.config, error.response?.status || 'NETWORK_ERROR', duration);
throw error;
}
);
// Circuit breaker
this.breaker = new CircuitBreaker(
(config) => this.client.request(config),
{
timeout: this.timeout,
errorThresholdPercentage: 50,
resetTimeout: 30000,
volumeThreshold: 10
}
);
this.setupCircuitBreakerEvents();
}
setupCircuitBreakerEvents() {
this.breaker.on('open', () => {
console.log(`Circuit OPEN for ${this.serviceName}`);
// Alert monitoring system
});
this.breaker.on('halfOpen', () => {
console.log(`Circuit HALF-OPEN for ${this.serviceName}`);
});
this.breaker.on('close', () => {
console.log(`Circuit CLOSED for ${this.serviceName}`);
});
}
async request(config) {
try {
const response = await this.breaker.fire(config);
return response.data;
} catch (error) {
throw this.handleError(error);
}
}
handleError(error) {
if (error.isAxiosError) {
if (error.response) {
// Server responded with error
return new ServiceError(
this.serviceName,
error.response.status,
error.response.data?.error?.message || 'Service error',
error.response.data?.error?.code
);
} else if (error.code === 'ECONNABORTED') {
// Timeout
return new ServiceError(
this.serviceName,
504,
'Request timeout',
'TIMEOUT'
);
} else {
// Network error
return new ServiceError(
this.serviceName,
503,
'Service unavailable',
'NETWORK_ERROR'
);
}
}
if (error.message?.includes('Breaker is open')) {
return new ServiceError(
this.serviceName,
503,
'Service temporarily unavailable',
'CIRCUIT_OPEN'
);
}
return error;
}
// Convenience methods
get(url, config = {}) {
return this.request({ method: 'get', url, ...config });
}
post(url, data, config = {}) {
return this.request({ method: 'post', url, data, ...config });
}
put(url, data, config = {}) {
return this.request({ method: 'put', url, data, ...config });
}
delete(url, config = {}) {
return this.request({ method: 'delete', url, ...config });
}
}
// Custom Error Class
class ServiceError extends Error {
constructor(serviceName, statusCode, message, code) {
super(message);
this.serviceName = serviceName;
this.statusCode = statusCode;
this.code = code;
this.isServiceError = true;
}
}
User Service Client Example
Copy
class UserServiceClient extends ServiceClient {
constructor() {
super({
serviceName: 'user-service',
baseURL: process.env.USER_SERVICE_URL || 'http://user-service:3001',
timeout: 5000
});
}
async getUser(userId) {
try {
const response = await this.get(`/users/${userId}`);
return response.data;
} catch (error) {
if (error.statusCode === 404) {
return null;
}
throw error;
}
}
async validateUser(userId) {
const user = await this.getUser(userId);
return user !== null;
}
async getUsersByIds(userIds) {
const response = await this.post('/users/batch', { ids: userIds });
return response.data;
}
// With retry logic for specific operations
async createUser(userData) {
return this.withRetry(
() => this.post('/users', userData),
{
retries: 3,
retryOn: [503, 502, 504],
backoff: 'exponential'
}
);
}
async withRetry(operation, options) {
const { retries, retryOn, backoff } = options;
let lastError;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (!retryOn.includes(error.statusCode)) {
throw error;
}
if (attempt < retries) {
const delay = backoff === 'exponential'
? Math.pow(2, attempt) * 100
: 100 * attempt;
await this.sleep(delay);
}
}
}
throw lastError;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
module.exports = new UserServiceClient();
gRPC Communication
gRPC offers high-performance, type-safe communication between services.Protocol Buffers Definition
Copy
// protos/user.proto
syntax = "proto3";
package user;
service UserService {
// Unary RPC
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
rpc UpdateUser(UpdateUserRequest) returns (User);
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
// Server streaming
rpc ListUsers(ListUsersRequest) returns (stream User);
// Client streaming
rpc BatchCreateUsers(stream CreateUserRequest) returns (BatchCreateResponse);
// Bidirectional streaming
rpc ChatUsers(stream UserMessage) returns (stream UserMessage);
}
message User {
string id = 1;
string email = 2;
string name = 3;
UserStatus status = 4;
google.protobuf.Timestamp created_at = 5;
UserPreferences preferences = 6;
}
message UserPreferences {
string language = 1;
string timezone = 2;
bool notifications_enabled = 3;
}
enum UserStatus {
UNKNOWN = 0;
ACTIVE = 1;
INACTIVE = 2;
SUSPENDED = 3;
}
message GetUserRequest {
string id = 1;
}
message CreateUserRequest {
string email = 1;
string name = 2;
optional string password = 3;
}
message UpdateUserRequest {
string id = 1;
optional string email = 2;
optional string name = 3;
optional UserStatus status = 4;
}
message DeleteUserRequest {
string id = 1;
}
message DeleteUserResponse {
bool success = 1;
}
message ListUsersRequest {
int32 page = 1;
int32 limit = 2;
optional UserStatus status = 3;
}
message BatchCreateResponse {
int32 created_count = 1;
repeated string created_ids = 2;
}
message UserMessage {
string user_id = 1;
string content = 2;
google.protobuf.Timestamp timestamp = 3;
}
gRPC Server Implementation
Copy
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
// Load protobuf
const PROTO_PATH = path.join(__dirname, '../protos/user.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const userProto = grpc.loadPackageDefinition(packageDefinition).user;
// Service Implementation
class UserGrpcService {
constructor(userRepository) {
this.userRepository = userRepository;
}
// Unary RPC
async getUser(call, callback) {
try {
const user = await this.userRepository.findById(call.request.id);
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
message: `User ${call.request.id} not found`
});
}
callback(null, this.toProtoUser(user));
} catch (error) {
callback({
code: grpc.status.INTERNAL,
message: error.message
});
}
}
async createUser(call, callback) {
try {
const user = await this.userRepository.create({
email: call.request.email,
name: call.request.name,
password: call.request.password
});
callback(null, this.toProtoUser(user));
} catch (error) {
if (error.code === 'DUPLICATE_EMAIL') {
return callback({
code: grpc.status.ALREADY_EXISTS,
message: 'Email already exists'
});
}
callback({
code: grpc.status.INTERNAL,
message: error.message
});
}
}
// Server Streaming
async listUsers(call) {
try {
const { page, limit, status } = call.request;
const cursor = this.userRepository.findAllCursor({ page, limit, status });
for await (const user of cursor) {
call.write(this.toProtoUser(user));
}
call.end();
} catch (error) {
call.emit('error', {
code: grpc.status.INTERNAL,
message: error.message
});
}
}
// Client Streaming
async batchCreateUsers(call, callback) {
const createdIds = [];
call.on('data', async (request) => {
try {
const user = await this.userRepository.create({
email: request.email,
name: request.name
});
createdIds.push(user.id);
} catch (error) {
console.error('Failed to create user:', error);
}
});
call.on('end', () => {
callback(null, {
created_count: createdIds.length,
created_ids: createdIds
});
});
call.on('error', (error) => {
callback({
code: grpc.status.INTERNAL,
message: error.message
});
});
}
toProtoUser(user) {
return {
id: user.id,
email: user.email,
name: user.name,
status: user.status.toUpperCase(),
created_at: {
seconds: Math.floor(user.createdAt.getTime() / 1000),
nanos: (user.createdAt.getTime() % 1000) * 1000000
},
preferences: user.preferences || {}
};
}
}
// Start Server
function startGrpcServer(port = 50051) {
const server = new grpc.Server();
const userService = new UserGrpcService(userRepository);
server.addService(userProto.UserService.service, {
getUser: userService.getUser.bind(userService),
createUser: userService.createUser.bind(userService),
listUsers: userService.listUsers.bind(userService),
batchCreateUsers: userService.batchCreateUsers.bind(userService)
});
server.bindAsync(
`0.0.0.0:${port}`,
grpc.ServerCredentials.createInsecure(),
(error, port) => {
if (error) {
console.error('Failed to start gRPC server:', error);
return;
}
console.log(`gRPC server running on port ${port}`);
server.start();
}
);
return server;
}
module.exports = { startGrpcServer };
gRPC Client Implementation
Copy
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
class UserGrpcClient {
constructor(address = 'localhost:50051') {
const PROTO_PATH = path.join(__dirname, '../protos/user.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const userProto = grpc.loadPackageDefinition(packageDefinition).user;
this.client = new userProto.UserService(
address,
grpc.credentials.createInsecure(),
{
'grpc.keepalive_time_ms': 10000,
'grpc.keepalive_timeout_ms': 5000,
'grpc.keepalive_permit_without_calls': 1
}
);
// Promisify methods
this.getUser = this.promisify(this.client.getUser);
this.createUser = this.promisify(this.client.createUser);
}
promisify(method) {
return (request) => {
return new Promise((resolve, reject) => {
method.call(this.client, request, (error, response) => {
if (error) {
reject(this.handleError(error));
} else {
resolve(response);
}
});
});
};
}
// Server streaming
async *listUsers(request) {
const call = this.client.listUsers(request);
for await (const user of call) {
yield user;
}
}
// Client streaming
async batchCreateUsers(users) {
return new Promise((resolve, reject) => {
const call = this.client.batchCreateUsers((error, response) => {
if (error) {
reject(this.handleError(error));
} else {
resolve(response);
}
});
for (const user of users) {
call.write(user);
}
call.end();
});
}
handleError(error) {
const errorMap = {
[grpc.status.NOT_FOUND]: 404,
[grpc.status.ALREADY_EXISTS]: 409,
[grpc.status.INVALID_ARGUMENT]: 400,
[grpc.status.PERMISSION_DENIED]: 403,
[grpc.status.UNAUTHENTICATED]: 401,
[grpc.status.UNAVAILABLE]: 503,
[grpc.status.DEADLINE_EXCEEDED]: 504
};
return {
statusCode: errorMap[error.code] || 500,
message: error.details || error.message,
code: grpc.status[error.code]
};
}
close() {
grpc.closeClient(this.client);
}
}
module.exports = UserGrpcClient;
REST vs gRPC Comparison
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ REST vs gRPC COMPARISON │
├─────────────────┬───────────────────────────────────────────────────────────┤
│ Aspect │ REST │ gRPC │
├─────────────────┼───────────────────────────────┼───────────────────────────┤
│ Protocol │ HTTP/1.1 (or HTTP/2) │ HTTP/2 only │
│ Payload Format │ JSON (human-readable) │ Protocol Buffers (binary) │
│ Performance │ Slower (text parsing) │ ~7x faster │
│ Payload Size │ Larger │ ~3x smaller │
│ Streaming │ Limited (SSE, WebSocket) │ Native bi-directional │
│ Type Safety │ Weak (runtime validation) │ Strong (compile-time) │
│ Browser Support │ Native │ Requires gRPC-Web │
│ Tooling │ Extensive │ Limited but growing │
│ Debugging │ Easy (curl, Postman) │ Harder (need special tools)│
│ Learning Curve │ Low │ Medium │
│ Documentation │ OpenAPI/Swagger │ Protobuf files │
└─────────────────┴───────────────────────────────┴───────────────────────────┘
When to Use What
- Use REST
- Use gRPC
Best for:
- Public APIs consumed by browsers
- Third-party integrations
- Simple CRUD operations
- When human readability matters
- Mobile apps (better tooling)
- When HTTP caching is valuable
- Customer-facing API
- Webhook integrations
- Admin dashboards
- Simple service communication
Best for:
- Internal service communication
- High-performance requirements
- Streaming data (real-time)
- Polyglot environments
- Large payload transfers
- Strict contract enforcement
- Service mesh communication
- Real-time data processing
- Inter-datacenter communication
- ML model serving
Handling Failures
Timeout Configuration
Copy
// Tiered Timeout Strategy
const timeoutConfig = {
// Fast operations (cached, simple queries)
fast: {
timeout: 1000,
services: ['cache-service', 'feature-flags']
},
// Standard operations (database reads)
standard: {
timeout: 5000,
services: ['user-service', 'product-service']
},
// Slow operations (complex queries, external APIs)
slow: {
timeout: 30000,
services: ['report-service', 'payment-service']
},
// Very slow operations (batch processing)
background: {
timeout: 120000,
services: ['export-service', 'analytics-service']
}
};
// Timeout with different strategies per endpoint
const endpointTimeouts = {
'GET /users/:id': 2000, // Fast read
'GET /users': 5000, // List with pagination
'POST /users': 10000, // Create with validation
'GET /users/:id/orders': 15000 // Complex query
};
Retry Strategies
Copy
const RetryStrategies = {
// Exponential backoff with jitter
exponentialBackoff: async (operation, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries || !isRetryable(error)) {
throw error;
}
const baseDelay = Math.pow(2, attempt) * 100;
const jitter = Math.random() * 100;
await sleep(baseDelay + jitter);
}
}
},
// Linear backoff
linearBackoff: async (operation, maxRetries = 3, delay = 1000) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries || !isRetryable(error)) {
throw error;
}
await sleep(delay * attempt);
}
}
},
// Immediate retry (for transient failures)
immediateRetry: async (operation, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries || !isRetryable(error)) {
throw error;
}
}
}
}
};
// Retryable error detection
function isRetryable(error) {
const retryableCodes = [
408, // Request Timeout
429, // Too Many Requests
500, // Internal Server Error
502, // Bad Gateway
503, // Service Unavailable
504 // Gateway Timeout
];
return retryableCodes.includes(error.statusCode) ||
error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT';
}
// Usage
const user = await RetryStrategies.exponentialBackoff(
() => userService.getUser(userId)
);
Fallback Strategies
Copy
class UserServiceWithFallback {
constructor(userClient, cache, defaults) {
this.userClient = userClient;
this.cache = cache;
this.defaults = defaults;
}
async getUser(userId) {
try {
// 1. Try primary service
const user = await this.userClient.getUser(userId);
// Cache for fallback
await this.cache.set(`user:${userId}`, user, { ttl: 3600 });
return user;
} catch (error) {
// 2. Try cache fallback
const cached = await this.cache.get(`user:${userId}`);
if (cached) {
console.log(`Using cached user for ${userId}`);
return { ...cached, _fromCache: true };
}
// 3. Try default fallback
if (this.defaults.enabled) {
console.log(`Using default user for ${userId}`);
return {
id: userId,
name: 'Unknown User',
email: '[email protected]',
_isDefault: true
};
}
throw error;
}
}
async getUserWithGracefulDegradation(userId, options = {}) {
const { requireFresh = false, acceptPartial = true } = options;
try {
return await this.userClient.getUser(userId);
} catch (error) {
if (requireFresh) {
throw error;
}
// Return partial data from cache
if (acceptPartial) {
const partialData = await this.getPartialFromCache(userId);
if (partialData) {
return { ...partialData, _partial: true };
}
}
throw error;
}
}
}
Request Correlation & Tracing
Copy
const { v4: uuid } = require('uuid');
const asyncHooks = require('async_hooks');
// Context storage for request correlation
class RequestContext {
static storage = new Map();
static init() {
const asyncHook = asyncHooks.createHook({
init: (asyncId, type, triggerAsyncId) => {
const parentContext = this.storage.get(triggerAsyncId);
if (parentContext) {
this.storage.set(asyncId, parentContext);
}
},
destroy: (asyncId) => {
this.storage.delete(asyncId);
}
});
asyncHook.enable();
}
static set(context) {
this.storage.set(asyncHooks.executionAsyncId(), context);
}
static get() {
return this.storage.get(asyncHooks.executionAsyncId());
}
}
// Middleware to create correlation context
const correlationMiddleware = (req, res, next) => {
const context = {
requestId: req.headers['x-request-id'] || uuid(),
correlationId: req.headers['x-correlation-id'] || uuid(),
userId: req.user?.id,
service: process.env.SERVICE_NAME,
startTime: Date.now()
};
RequestContext.set(context);
// Add to response headers
res.set('X-Request-ID', context.requestId);
res.set('X-Correlation-ID', context.correlationId);
next();
};
// Automatically propagate to outgoing requests
class CorrelatedHttpClient {
constructor(baseURL) {
this.client = axios.create({ baseURL });
this.client.interceptors.request.use((config) => {
const context = RequestContext.get();
if (context) {
config.headers['X-Request-ID'] = uuid(); // New request ID
config.headers['X-Correlation-ID'] = context.correlationId; // Same correlation
config.headers['X-Parent-Request-ID'] = context.requestId;
}
return config;
});
}
}
Interview Questions
Q1: How do you handle partial failures in synchronous communication?
Q1: How do you handle partial failures in synchronous communication?
Answer:
- Circuit Breaker: Stop calling failing services temporarily
- Timeouts: Fail fast rather than waiting forever
- Retries with Backoff: Retry transient failures with exponential backoff
- Fallbacks: Return cached/default data when service is down
- Graceful Degradation: Partial functionality is better than complete failure
Q2: When would you choose gRPC over REST?
Q2: When would you choose gRPC over REST?
Answer:Choose gRPC when:
- Internal service-to-service communication
- Performance is critical (10x+ improvement)
- Need streaming (bidirectional)
- Strict API contracts required
- Polyglot services (auto-generated clients)
- Public-facing APIs
- Browser clients (without gRPC-Web)
- Simple CRUD operations
- Need HTTP caching
- Debugging simplicity matters
Q3: How do you version APIs in microservices?
Q3: How do you version APIs in microservices?
Answer:Common strategies:
- URL Path:
/api/v1/users(most common) - Query Parameter:
/api/users?version=1 - Header:
Accept: application/vnd.api.v1+json - Content Negotiation: Custom media types
- Support 2-3 versions simultaneously
- Deprecate gracefully with warnings
- Document migration paths
- Use semantic versioning
- Breaking changes = major version
Q4: How do you implement request tracing across services?
Q4: How do you implement request tracing across services?
Answer:Implementation:
- Correlation ID: Unique ID that flows through all services for one request
- Request ID: Unique per service call (for individual tracing)
- Propagation: Headers like
X-Correlation-ID,X-Request-ID - Distributed Tracing: Tools like Jaeger, Zipkin, or OpenTelemetry
X-Correlation-ID: Same for entire request chainX-Request-ID: Unique per hopX-Parent-Request-ID: Previous service’s request IDX-B3-TraceId: Zipkin trace ID
Summary
Key Takeaways
- REST for public/browser APIs, gRPC for internal high-performance
- Always implement timeouts, retries, and fallbacks
- Use correlation IDs for tracing
- Version your APIs properly
- Circuit breakers prevent cascade failures
Next Steps
In the next chapter, we’ll dive into Asynchronous Communication with message queues and event-driven architecture.