Skip to main content

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

// 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

// 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

// 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

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

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

// 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

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

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

┌─────────────────────────────────────────────────────────────────────────────┐
│                         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

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
Example Use Cases:
  • Customer-facing API
  • Webhook integrations
  • Admin dashboards
  • Simple service communication

Handling Failures

Timeout Configuration

// 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

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

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

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

Answer:
  1. Circuit Breaker: Stop calling failing services temporarily
  2. Timeouts: Fail fast rather than waiting forever
  3. Retries with Backoff: Retry transient failures with exponential backoff
  4. Fallbacks: Return cached/default data when service is down
  5. Graceful Degradation: Partial functionality is better than complete failure
Example: If user service is down, order service can still work with cached user data or proceed with just the user ID.
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)
Stay with REST when:
  • Public-facing APIs
  • Browser clients (without gRPC-Web)
  • Simple CRUD operations
  • Need HTTP caching
  • Debugging simplicity matters
Answer:Common strategies:
  1. URL Path: /api/v1/users (most common)
  2. Query Parameter: /api/users?version=1
  3. Header: Accept: application/vnd.api.v1+json
  4. Content Negotiation: Custom media types
Best practices:
  • Support 2-3 versions simultaneously
  • Deprecate gracefully with warnings
  • Document migration paths
  • Use semantic versioning
  • Breaking changes = major version
Answer:Implementation:
  1. Correlation ID: Unique ID that flows through all services for one request
  2. Request ID: Unique per service call (for individual tracing)
  3. Propagation: Headers like X-Correlation-ID, X-Request-ID
  4. Distributed Tracing: Tools like Jaeger, Zipkin, or OpenTelemetry
Key headers:
  • X-Correlation-ID: Same for entire request chain
  • X-Request-ID: Unique per hop
  • X-Parent-Request-ID: Previous service’s request ID
  • X-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.