Node.js Microservices Architecture: Design Patterns and Best Practices

Node.js, Microservices, Architecture, Design Patterns|SEPTEMBER 10, 2025|1 VIEWS
Master microservices architecture with Node.js - from design patterns to production deployment

Introduction

Microservices architecture has revolutionized how we build and scale applications. By breaking down monolithic applications into smaller, independent services, teams can develop, deploy, and scale components independently. This comprehensive guide explores microservices architecture with Node.js, covering essential design patterns, best practices, and real-world implementation strategies.

Understanding Microservices Architecture

What are Microservices?

Microservices are an architectural approach where applications are composed of small, independent services that communicate over well-defined APIs. Each service is:

  • Independently deployable
  • Loosely coupled
  • Organized around business capabilities
  • Owned by a small team

Monoliths vs Microservices

Monolithic Architecture

// Single application handling all functionality
const express = require('express');
const app = express();

// User management
app.post('/users', handleUserCreation);
app.get('/users/:id', getUserById);

// Order processing
app.post('/orders', createOrder);
app.get('/orders/:id', getOrderById);

// Payment processing
app.post('/payments', processPayment);

// Inventory management
app.put('/inventory/:id', updateInventory);

app.listen(3000);

Microservices Architecture

// User Service (Port 3001)
const userApp = express();
userApp.post('/users', handleUserCreation);
userApp.get('/users/:id', getUserById);
userApp.listen(3001);

// Order Service (Port 3002)
const orderApp = express();
orderApp.post('/orders', createOrder);
orderApp.get('/orders/:id', getOrderById);
orderApp.listen(3002);

// Payment Service (Port 3003)
const paymentApp = express();
paymentApp.post('/payments', processPayment);
paymentApp.listen(3003);

Core Design Patterns

1. API Gateway Pattern

The API Gateway acts as a single entry point for all client requests, routing them to appropriate microservices.

// api-gateway/server.js
const express = require('express');
const httpProxy = require('http-proxy-middleware');
const app = express();

// Route to User Service
app.use(
  '/api/users',
  httpProxy({
    target: 'http://user-service:3001',
    changeOrigin: true,
    pathRewrite: {
      '^/api/users': '/users',
    },
  })
);

// Route to Order Service
app.use(
  '/api/orders',
  httpProxy({
    target: 'http://order-service:3002',
    changeOrigin: true,
    pathRewrite: {
      '^/api/orders': '/orders',
    },
  })
);

// Route to Payment Service
app.use(
  '/api/payments',
  httpProxy({
    target: 'http://payment-service:3003',
    changeOrigin: true,
    pathRewrite: {
      '^/api/payments': '/payments',
    },
  })
);

app.listen(3000, () => {
  console.log('API Gateway running on port 3000');
});

Advanced API Gateway with Authentication

// api-gateway/middleware/auth.js
const jwt = require('jsonwebtoken');

const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'Access denied' });
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid token' });
    }
    req.user = user;
    next();
  });
};

// Apply authentication to protected routes
app.use('/api/orders', authenticateToken);
app.use('/api/payments', authenticateToken);

2. Service Discovery Pattern

Service discovery helps services find and communicate with each other dynamically.

// service-registry/registry.js
class ServiceRegistry {
  constructor() {
    this.services = new Map();
  }

  register(serviceName, serviceUrl, healthCheckUrl) {
    this.services.set(serviceName, {
      url: serviceUrl,
      healthCheck: healthCheckUrl,
      lastSeen: Date.now(),
    });
    console.log(`Service ${serviceName} registered at ${serviceUrl}`);
  }

  discover(serviceName) {
    const service = this.services.get(serviceName);
    if (!service) {
      throw new Error(`Service ${serviceName} not found`);
    }
    return service;
  }

  unregister(serviceName) {
    this.services.delete(serviceName);
    console.log(`Service ${serviceName} unregistered`);
  }

  getAllServices() {
    return Array.from(this.services.entries());
  }
}

module.exports = new ServiceRegistry();

Service Registration Example

// user-service/server.js
const express = require('express');
const axios = require('axios');
const app = express();

const SERVICE_NAME = 'user-service';
const SERVICE_URL = 'http://localhost:3001';
const REGISTRY_URL = 'http://localhost:3000';

// Register service on startup
const registerService = async () => {
  try {
    await axios.post(`${REGISTRY_URL}/register`, {
      name: SERVICE_NAME,
      url: SERVICE_URL,
      healthCheck: `${SERVICE_URL}/health`,
    });
    console.log('Service registered successfully');
  } catch (error) {
    console.error('Failed to register service:', error.message);
  }
};

// Health check endpoint
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'healthy', timestamp: Date.now() });
});

app.listen(3001, () => {
  console.log('User service running on port 3001');
  registerService();
});

3. Circuit Breaker Pattern

Prevent cascading failures by monitoring service health and stopping requests to failing services.

// utils/circuit-breaker.js
class CircuitBreaker {
  constructor(request, options = {}) {
    this.request = request;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failureCount = 0;
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 60000;
    this.monitor = options.monitor || false;
  }

  async call(...args) {
    if (this.state === 'OPEN') {
      if (this.nextAttempt <= Date.now()) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await this.request(...args);
      return this.onSuccess(result);
    } catch (error) {
      return this.onFailure(error);
    }
  }

  onSuccess(result) {
    this.failureCount = 0;
    if (this.state === 'HALF_OPEN') {
      this.state = 'CLOSED';
    }
    return result;
  }

  onFailure(error) {
    this.failureCount++;
    if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
    }
    throw error;
  }
}

module.exports = CircuitBreaker;

Using Circuit Breaker

// order-service/services/paymentService.js
const axios = require('axios');
const CircuitBreaker = require('../utils/circuit-breaker');

const paymentRequest = async (paymentData) => {
  const response = await axios.post(
    'http://payment-service:3003/payments',
    paymentData
  );
  return response.data;
};

const paymentCircuitBreaker = new CircuitBreaker(paymentRequest, {
  failureThreshold: 3,
  resetTimeout: 30000,
});

const processPayment = async (paymentData) => {
  try {
    return await paymentCircuitBreaker.call(paymentData);
  } catch (error) {
    console.error('Payment service unavailable:', error.message);
    // Implement fallback logic
    return { status: 'pending', message: 'Payment will be processed later' };
  }
};

module.exports = { processPayment };

4. Event-Driven Architecture

Use events to enable loose coupling between services.

// shared/eventBus.js
const EventEmitter = require('events');
const Redis = require('redis');

class EventBus extends EventEmitter {
  constructor() {
    super();
    this.redisClient = Redis.createClient();
    this.subscriber = Redis.createClient();
    this.setupRedisSubscriber();
  }

  async publish(event, data) {
    const eventData = {
      event,
      data,
      timestamp: Date.now(),
      id: this.generateId(),
    };

    // Emit locally
    this.emit(event, eventData);

    // Publish to Redis for other services
    await this.redisClient.publish(
      'microservices-events',
      JSON.stringify(eventData)
    );
  }

  setupRedisSubscriber() {
    this.subscriber.subscribe('microservices-events');
    this.subscriber.on('message', (channel, message) => {
      try {
        const eventData = JSON.parse(message);
        this.emit(eventData.event, eventData);
      } catch (error) {
        console.error('Error parsing event:', error);
      }
    });
  }

  generateId() {
    return Math.random().toString(36).substr(2, 9);
  }
}

module.exports = new EventBus();

Event Handlers

// order-service/handlers/orderEvents.js
const eventBus = require('../shared/eventBus');
const emailService = require('../services/emailService');
const inventoryService = require('../services/inventoryService');

// Handle order creation
eventBus.on('order.created', async (eventData) => {
  const { order } = eventData.data;

  try {
    // Update inventory
    await inventoryService.reserveItems(order.items);

    // Send confirmation email
    await emailService.sendOrderConfirmation(order.customerEmail, order);

    console.log(`Order ${order.id} processed successfully`);
  } catch (error) {
    console.error(`Failed to process order ${order.id}:`, error);
    // Publish compensation event
    await eventBus.publish('order.processing.failed', {
      order,
      error: error.message,
    });
  }
});

// Handle payment confirmation
eventBus.on('payment.confirmed', async (eventData) => {
  const { orderId, paymentId } = eventData.data;

  try {
    // Update order status
    await orderService.updateOrderStatus(orderId, 'paid');

    // Trigger fulfillment
    await eventBus.publish('order.fulfillment.requested', {
      orderId,
      paymentId,
    });
  } catch (error) {
    console.error(`Failed to handle payment confirmation:`, error);
  }
});

5. Saga Pattern

Manage distributed transactions across multiple services.

// order-service/sagas/orderSaga.js
class OrderSaga {
  constructor() {
    this.steps = [];
    this.compensations = [];
  }

  addStep(stepFunction, compensationFunction) {
    this.steps.push(stepFunction);
    this.compensations.unshift(compensationFunction); // Reverse order for rollback
  }

  async execute(data) {
    const completedSteps = [];

    try {
      for (let i = 0; i < this.steps.length; i++) {
        const result = await this.steps[i](data);
        completedSteps.push(i);
        data = { ...data, ...result };
      }
      return data;
    } catch (error) {
      console.error('Saga execution failed:', error);
      await this.compensate(completedSteps, data);
      throw error;
    }
  }

  async compensate(completedSteps, data) {
    for (const stepIndex of completedSteps.reverse()) {
      try {
        await this.compensations[stepIndex](data);
      } catch (compensationError) {
        console.error(
          `Compensation failed for step ${stepIndex}:`,
          compensationError
        );
      }
    }
  }
}

// Usage example
const createOrderSaga = new OrderSaga();

createOrderSaga.addStep(
  // Step: Reserve inventory
  async (data) => {
    const reservation = await inventoryService.reserve(data.items);
    return { reservationId: reservation.id };
  },
  // Compensation: Release inventory
  async (data) => {
    if (data.reservationId) {
      await inventoryService.release(data.reservationId);
    }
  }
);

createOrderSaga.addStep(
  // Step: Process payment
  async (data) => {
    const payment = await paymentService.charge(data.paymentInfo);
    return { paymentId: payment.id };
  },
  // Compensation: Refund payment
  async (data) => {
    if (data.paymentId) {
      await paymentService.refund(data.paymentId);
    }
  }
);

createOrderSaga.addStep(
  // Step: Create order
  async (data) => {
    const order = await orderService.create(data);
    return { orderId: order.id };
  },
  // Compensation: Cancel order
  async (data) => {
    if (data.orderId) {
      await orderService.cancel(data.orderId);
    }
  }
);

module.exports = { createOrderSaga };

Service Communication Patterns

1. Synchronous Communication (HTTP/REST)

// user-service/controllers/userController.js
const express = require('express');
const axios = require('axios');
const router = express.Router();

// Get user with order history
router.get('/:id/orders', async (req, res) => {
  try {
    const userId = req.params.id;

    // Get user information
    const user = await userService.findById(userId);
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }

    // Get orders from order service
    const ordersResponse = await axios.get(
      `http://order-service:3002/orders/user/${userId}`
    );
    const orders = ordersResponse.data;

    res.json({
      user,
      orders,
    });
  } catch (error) {
    console.error('Error fetching user orders:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

module.exports = router;

2. Asynchronous Communication (Message Queues)

// shared/messageQueue.js
const amqp = require('amqplib');

class MessageQueue {
  constructor() {
    this.connection = null;
    this.channel = null;
  }

  async connect() {
    try {
      this.connection = await amqp.connect(process.env.RABBITMQ_URL);
      this.channel = await this.connection.createChannel();
      console.log('Connected to RabbitMQ');
    } catch (error) {
      console.error('Failed to connect to RabbitMQ:', error);
      throw error;
    }
  }

  async publishToQueue(queueName, message) {
    await this.channel.assertQueue(queueName, { durable: true });
    this.channel.sendToQueue(queueName, Buffer.from(JSON.stringify(message)), {
      persistent: true,
    });
  }

  async consumeFromQueue(queueName, callback) {
    await this.channel.assertQueue(queueName, { durable: true });
    this.channel.prefetch(1);

    this.channel.consume(queueName, async (msg) => {
      if (msg) {
        try {
          const content = JSON.parse(msg.content.toString());
          await callback(content);
          this.channel.ack(msg);
        } catch (error) {
          console.error('Error processing message:', error);
          this.channel.nack(msg, false, false); // Dead letter the message
        }
      }
    });
  }
}

module.exports = new MessageQueue();

Using Message Queue

// notification-service/server.js
const messageQueue = require('../shared/messageQueue');
const emailService = require('./services/emailService');

const processNotifications = async () => {
  await messageQueue.connect();

  // Listen for order confirmation notifications
  await messageQueue.consumeFromQueue(
    'order-notifications',
    async (message) => {
      const { type, data } = message;

      switch (type) {
        case 'ORDER_CONFIRMED':
          await emailService.sendOrderConfirmation(data.email, data.order);
          break;
        case 'PAYMENT_PROCESSED':
          await emailService.sendPaymentConfirmation(data.email, data.payment);
          break;
        case 'ORDER_SHIPPED':
          await emailService.sendShippingNotification(
            data.email,
            data.tracking
          );
          break;
        default:
          console.log(`Unknown notification type: ${type}`);
      }
    }
  );
};

processNotifications();

Data Management Strategies

1. Database per Service

Each microservice manages its own database, ensuring loose coupling and independence.

// user-service/models/User.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  name: { type: String, required: true },
  passwordHash: { type: String, required: true },
  createdAt: { type: Date, default: Date.now },
  profile: {
    firstName: String,
    lastName: String,
    phone: String,
    address: {
      street: String,
      city: String,
      country: String,
      zipCode: String,
    },
  },
});

module.exports = mongoose.model('User', userSchema);
// order-service/models/Order.js
const mongoose = require('mongoose');

const orderSchema = new mongoose.Schema({
  userId: { type: String, required: true }, // Reference to user service
  items: [
    {
      productId: String,
      quantity: Number,
      price: Number,
      name: String,
    },
  ],
  status: {
    type: String,
    enum: ['pending', 'confirmed', 'paid', 'shipped', 'delivered', 'cancelled'],
    default: 'pending',
  },
  totalAmount: { type: Number, required: true },
  shippingAddress: {
    street: String,
    city: String,
    country: String,
    zipCode: String,
  },
  createdAt: { type: Date, default: Date.now },
});

module.exports = mongoose.model('Order', orderSchema);

2. Data Synchronization

Implement eventual consistency through event-driven data synchronization.

// shared/dataSynchronizer.js
class DataSynchronizer {
  constructor(eventBus) {
    this.eventBus = eventBus;
    this.setupEventHandlers();
  }

  setupEventHandlers() {
    // Sync user data changes
    this.eventBus.on('user.updated', async (eventData) => {
      const { userId, changes } = eventData.data;

      // Update user data in order service cache
      await this.updateUserCache('order-service', userId, changes);

      // Update user data in notification service cache
      await this.updateUserCache('notification-service', userId, changes);
    });

    // Sync product data changes
    this.eventBus.on('product.updated', async (eventData) => {
      const { productId, changes } = eventData.data;

      // Update product cache in order service
      await this.updateProductCache('order-service', productId, changes);
    });
  }

  async updateUserCache(serviceName, userId, changes) {
    try {
      // Implementation depends on the caching strategy
      // Could be Redis, in-memory cache, or read replica
      console.log(`Updating user cache in ${serviceName} for user ${userId}`);
    } catch (error) {
      console.error(`Failed to update user cache in ${serviceName}:`, error);
    }
  }

  async updateProductCache(serviceName, productId, changes) {
    try {
      console.log(
        `Updating product cache in ${serviceName} for product ${productId}`
      );
    } catch (error) {
      console.error(`Failed to update product cache in ${serviceName}:`, error);
    }
  }
}

module.exports = DataSynchronizer;

Service Resilience and Monitoring

1. Health Checks

Implement comprehensive health checks for service monitoring.

// shared/healthCheck.js
class HealthChecker {
  constructor() {
    this.checks = new Map();
  }

  addCheck(name, checkFunction) {
    this.checks.set(name, checkFunction);
  }

  async runChecks() {
    const results = {};
    const promises = [];

    for (const [name, checkFunction] of this.checks) {
      promises.push(
        this.runSingleCheck(name, checkFunction).then((result) => {
          results[name] = result;
        })
      );
    }

    await Promise.all(promises);

    const overall = Object.values(results).every(
      (check) => check.status === 'healthy'
    )
      ? 'healthy'
      : 'unhealthy';

    return {
      status: overall,
      timestamp: new Date().toISOString(),
      checks: results,
    };
  }

  async runSingleCheck(name, checkFunction) {
    const startTime = Date.now();

    try {
      await checkFunction();
      return {
        status: 'healthy',
        responseTime: Date.now() - startTime,
      };
    } catch (error) {
      return {
        status: 'unhealthy',
        error: error.message,
        responseTime: Date.now() - startTime,
      };
    }
  }
}

module.exports = HealthChecker;

Service-specific Health Checks

// user-service/health.js
const HealthChecker = require('../shared/healthCheck');
const mongoose = require('mongoose');
const redis = require('redis');

const healthChecker = new HealthChecker();

// Database connectivity check
healthChecker.addCheck('database', async () => {
  if (mongoose.connection.readyState !== 1) {
    throw new Error('Database not connected');
  }

  // Test database query
  await mongoose.connection.db.admin().ping();
});

// Redis connectivity check
healthChecker.addCheck('redis', async () => {
  const client = redis.createClient();
  await client.ping();
  await client.quit();
});

// External service dependency check
healthChecker.addCheck('order-service', async () => {
  const axios = require('axios');
  const response = await axios.get('http://order-service:3002/health', {
    timeout: 5000,
  });

  if (response.status !== 200) {
    throw new Error('Order service unhealthy');
  }
});

module.exports = healthChecker;

2. Distributed Logging

Implement centralized logging with correlation IDs.

// shared/logger.js
const winston = require('winston');
const { v4: uuidv4 } = require('uuid');

class Logger {
  constructor(serviceName) {
    this.serviceName = serviceName;
    this.logger = winston.createLogger({
      level: 'info',
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
      ),
      defaultMeta: { service: serviceName },
      transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' }),
        new winston.transports.Console({
          format: winston.format.simple(),
        }),
      ],
    });
  }

  createCorrelationId() {
    return uuidv4();
  }

  info(message, meta = {}) {
    this.logger.info(message, meta);
  }

  error(message, error = null, meta = {}) {
    this.logger.error(message, {
      ...meta,
      error: error?.stack || error?.message || error,
    });
  }

  warn(message, meta = {}) {
    this.logger.warn(message, meta);
  }

  debug(message, meta = {}) {
    this.logger.debug(message, meta);
  }
}

// Middleware to add correlation ID to requests
const correlationMiddleware = (req, res, next) => {
  req.correlationId = req.headers['x-correlation-id'] || uuidv4();
  res.setHeader('x-correlation-id', req.correlationId);
  next();
};

module.exports = { Logger, correlationMiddleware };

3. Distributed Tracing

Implement distributed tracing to track requests across services.

// shared/tracing.js
const opentelemetry = require('@opentelemetry/api');
const { NodeTracerProvider } = require('@opentelemetry/sdk-node');
const { Resource } = require('@opentelemetry/resources');
const {
  SemanticResourceAttributes,
} = require('@opentelemetry/semantic-conventions');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const {
  ExpressInstrumentation,
} = require('@opentelemetry/instrumentation-express');

class TracingSetup {
  static initialize(serviceName) {
    const provider = new NodeTracerProvider({
      resource: new Resource({
        [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
        [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
      }),
    });

    const jaegerExporter = new JaegerExporter({
      endpoint:
        process.env.JAEGER_ENDPOINT || 'http://localhost:14268/api/traces',
    });

    provider.addSpanProcessor(new BatchSpanProcessor(jaegerExporter));
    provider.register();

    registerInstrumentations({
      instrumentations: [
        new HttpInstrumentation(),
        new ExpressInstrumentation(),
      ],
    });

    return opentelemetry.trace.getTracer(serviceName);
  }

  static createSpan(tracer, name, operation) {
    return tracer.startSpan(name, {
      kind: opentelemetry.SpanKind.INTERNAL,
      attributes: {
        'operation.type': operation,
      },
    });
  }
}

module.exports = TracingSetup;

Deployment and Orchestration

1. Docker Configuration

Create production-ready Docker images for each service.

# user-service/Dockerfile
FROM node:18-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodeuser -u 1001

# Change ownership
RUN chown -R nodeuser:nodejs /app
USER nodeuser

# Expose port
EXPOSE 3001

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js

# Start the application
CMD ["node", "server.js"]

Multi-stage Build for Production

# Build stage
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine AS production

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public

RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodeuser -u 1001

RUN chown -R nodeuser:nodejs /app
USER nodeuser

EXPOSE 3001

CMD ["node", "dist/server.js"]

2. Docker Compose for Development

# docker-compose.yml
version: '3.8'

services:
  api-gateway:
    build: ./api-gateway
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=development
    depends_on:
      - user-service
      - order-service
      - payment-service

  user-service:
    build: ./user-service
    ports:
      - '3001:3001'
    environment:
      - NODE_ENV=development
      - MONGODB_URL=mongodb://mongo:27017/users
      - REDIS_URL=redis://redis:6379
    depends_on:
      - mongo
      - redis

  order-service:
    build: ./order-service
    ports:
      - '3002:3002'
    environment:
      - NODE_ENV=development
      - MONGODB_URL=mongodb://mongo:27017/orders
      - REDIS_URL=redis://redis:6379
    depends_on:
      - mongo
      - redis

  payment-service:
    build: ./payment-service
    ports:
      - '3003:3003'
    environment:
      - NODE_ENV=development
      - MONGODB_URL=mongodb://mongo:27017/payments
    depends_on:
      - mongo

  mongo:
    image: mongo:5.0
    ports:
      - '27017:27017'
    volumes:
      - mongo_data:/data/db

  redis:
    image: redis:7-alpine
    ports:
      - '6379:6379'
    volumes:
      - redis_data:/data

  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - '5672:5672'
      - '15672:15672'
    environment:
      - RABBITMQ_DEFAULT_USER=admin
      - RABBITMQ_DEFAULT_PASS=password
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq

volumes:
  mongo_data:
  redis_data:
  rabbitmq_data:

3. Kubernetes Deployment

# k8s/user-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  labels:
    app: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
        - name: user-service
          image: user-service:latest
          ports:
            - containerPort: 3001
          env:
            - name: NODE_ENV
              value: 'production'
            - name: MONGODB_URL
              valueFrom:
                secretKeyRef:
                  name: db-secrets
                  key: mongodb-url
            - name: REDIS_URL
              valueFrom:
                configMapKeyRef:
                  name: app-config
                  key: redis-url
          livenessProbe:
            httpGet:
              path: /health
              port: 3001
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 3001
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            requests:
              memory: '128Mi'
              cpu: '100m'
            limits:
              memory: '256Mi'
              cpu: '200m'

---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3001
  type: ClusterIP

Security Best Practices

1. Service-to-Service Authentication

Implement JWT-based authentication between services.

// shared/serviceAuth.js
const jwt = require('jsonwebtoken');
const axios = require('axios');

class ServiceAuth {
  constructor() {
    this.serviceToken = null;
    this.tokenExpiry = null;
  }

  async getServiceToken() {
    if (this.serviceToken && this.tokenExpiry > Date.now()) {
      return this.serviceToken;
    }

    try {
      const response = await axios.post(
        'http://auth-service:3004/auth/service',
        {
          serviceId: process.env.SERVICE_ID,
          serviceSecret: process.env.SERVICE_SECRET,
        }
      );

      this.serviceToken = response.data.token;
      this.tokenExpiry = Date.now() + response.data.expiresIn * 1000;

      return this.serviceToken;
    } catch (error) {
      throw new Error(`Failed to get service token: ${error.message}`);
    }
  }

  async authenticatedRequest(url, options = {}) {
    const token = await this.getServiceToken();

    return axios({
      ...options,
      url,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${token}`,
        'X-Service-ID': process.env.SERVICE_ID,
      },
    });
  }
}

module.exports = new ServiceAuth();

2. API Rate Limiting

// shared/rateLimiter.js
const redis = require('redis');

class RateLimiter {
  constructor(redisClient) {
    this.redis = redisClient;
  }

  async isAllowed(key, maxRequests, windowSize) {
    const now = Date.now();
    const window = Math.floor(now / windowSize);
    const redisKey = `rate_limit:${key}:${window}`;

    const current = await this.redis.incr(redisKey);

    if (current === 1) {
      await this.redis.expire(redisKey, Math.ceil(windowSize / 1000));
    }

    return current <= maxRequests;
  }

  middleware(maxRequests = 100, windowSize = 60000) {
    return async (req, res, next) => {
      const key = req.ip || req.connection.remoteAddress;

      try {
        const allowed = await this.isAllowed(key, maxRequests, windowSize);

        if (!allowed) {
          return res.status(429).json({
            error: 'Too many requests',
            retryAfter: Math.ceil(windowSize / 1000),
          });
        }

        next();
      } catch (error) {
        console.error('Rate limiting error:', error);
        next(); // Fail open
      }
    };
  }
}

module.exports = RateLimiter;

3. Input Validation and Sanitization

// shared/validation.js
const Joi = require('joi');
const validator = require('validator');

class InputValidator {
  static validate(schema) {
    return (req, res, next) => {
      const { error, value } = schema.validate(req.body);

      if (error) {
        return res.status(400).json({
          error: 'Validation failed',
          details: error.details.map((detail) => ({
            field: detail.path.join('.'),
            message: detail.message,
          })),
        });
      }

      req.validatedBody = value;
      next();
    };
  }

  static sanitizeInput(input) {
    if (typeof input === 'string') {
      return validator.escape(input.trim());
    }

    if (Array.isArray(input)) {
      return input.map((item) => this.sanitizeInput(item));
    }

    if (typeof input === 'object' && input !== null) {
      const sanitized = {};
      for (const [key, value] of Object.entries(input)) {
        sanitized[key] = this.sanitizeInput(value);
      }
      return sanitized;
    }

    return input;
  }
}

// Validation schemas
const schemas = {
  createUser: Joi.object({
    email: Joi.string().email().required(),
    name: Joi.string().min(2).max(100).required(),
    password: Joi.string()
      .min(8)
      .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
      .required(),
    profile: Joi.object({
      firstName: Joi.string().max(50),
      lastName: Joi.string().max(50),
      phone: Joi.string().pattern(/^\+?[\d\s-()]+$/),
    }),
  }),

  createOrder: Joi.object({
    items: Joi.array()
      .items(
        Joi.object({
          productId: Joi.string().required(),
          quantity: Joi.number().integer().min(1).required(),
          price: Joi.number().positive().required(),
        })
      )
      .min(1)
      .required(),
    shippingAddress: Joi.object({
      street: Joi.string().required(),
      city: Joi.string().required(),
      country: Joi.string().required(),
      zipCode: Joi.string().required(),
    }).required(),
  }),
};

module.exports = { InputValidator, schemas };

Performance Optimization

1. Caching Strategies

Implement multi-level caching for improved performance.

// shared/cache.js
const redis = require('redis');

class CacheManager {
  constructor() {
    this.redisClient = redis.createClient();
    this.localCache = new Map();
    this.maxLocalCacheSize = 1000;
  }

  async get(key, options = {}) {
    const { useLocal = true, ttl = 300 } = options;

    // Try local cache first
    if (useLocal && this.localCache.has(key)) {
      const cached = this.localCache.get(key);
      if (cached.expiry > Date.now()) {
        return cached.value;
      }
      this.localCache.delete(key);
    }

    // Try Redis cache
    try {
      const value = await this.redisClient.get(key);
      if (value) {
        const parsed = JSON.parse(value);

        // Store in local cache
        if (useLocal) {
          this.setLocal(key, parsed, ttl);
        }

        return parsed;
      }
    } catch (error) {
      console.error('Redis cache error:', error);
    }

    return null;
  }

  async set(key, value, options = {}) {
    const { ttl = 300, useLocal = true } = options;

    // Set in Redis
    try {
      await this.redisClient.setex(key, ttl, JSON.stringify(value));
    } catch (error) {
      console.error('Redis cache set error:', error);
    }

    // Set in local cache
    if (useLocal) {
      this.setLocal(key, value, ttl);
    }
  }

  setLocal(key, value, ttl) {
    if (this.localCache.size >= this.maxLocalCacheSize) {
      // Remove oldest entry
      const firstKey = this.localCache.keys().next().value;
      this.localCache.delete(firstKey);
    }

    this.localCache.set(key, {
      value,
      expiry: Date.now() + ttl * 1000,
    });
  }

  async invalidate(pattern) {
    // Clear matching keys from local cache
    for (const key of this.localCache.keys()) {
      if (key.includes(pattern)) {
        this.localCache.delete(key);
      }
    }

    // Clear matching keys from Redis
    try {
      const keys = await this.redisClient.keys(`*${pattern}*`);
      if (keys.length > 0) {
        await this.redisClient.del(keys);
      }
    } catch (error) {
      console.error('Redis cache invalidation error:', error);
    }
  }
}

module.exports = new CacheManager();

2. Database Optimization

// shared/database.js
const mongoose = require('mongoose');

class DatabaseOptimizer {
  static setupConnection(uri, options = {}) {
    const defaultOptions = {
      maxPoolSize: 10,
      serverSelectionTimeoutMS: 5000,
      socketTimeoutMS: 45000,
      bufferMaxEntries: 0,
      useNewUrlParser: true,
      useUnifiedTopology: true,
    };

    return mongoose.connect(uri, { ...defaultOptions, ...options });
  }

  static createIndexes(model, indexes) {
    indexes.forEach((index) => {
      model.createIndex(index.fields, index.options);
    });
  }

  static setupReadPreference(schema, preference = 'secondary') {
    schema.set('read', preference);
  }

  // Pagination helper
  static async paginate(model, query = {}, options = {}) {
    const {
      page = 1,
      limit = 10,
      sort = { createdAt: -1 },
      populate = null,
    } = options;

    const skip = (page - 1) * limit;

    const [results, total] = await Promise.all([
      model
        .find(query)
        .sort(sort)
        .skip(skip)
        .limit(limit)
        .populate(populate)
        .lean(),
      model.countDocuments(query),
    ]);

    return {
      results,
      pagination: {
        page,
        limit,
        total,
        pages: Math.ceil(total / limit),
      },
    };
  }
}

module.exports = DatabaseOptimizer;

Testing Strategies

1. Unit Testing

// user-service/tests/services/userService.test.js
const { describe, it, beforeEach, afterEach } = require('mocha');
const { expect } = require('chai');
const sinon = require('sinon');
const UserService = require('../../services/userService');
const User = require('../../models/User');

describe('UserService', () => {
  let userService;
  let userModelStub;

  beforeEach(() => {
    userService = new UserService();
    userModelStub = sinon.stub(User);
  });

  afterEach(() => {
    sinon.restore();
  });

  describe('createUser', () => {
    it('should create a user successfully', async () => {
      const userData = {
        email: 'test@example.com',
        name: 'Test User',
        password: 'password123',
      };

      const expectedUser = { id: '123', ...userData };
      userModelStub.create.resolves(expectedUser);

      const result = await userService.createUser(userData);

      expect(result).to.deep.equal(expectedUser);
      expect(userModelStub.create.calledOnce).to.be.true;
      expect(userModelStub.create.calledWith(userData)).to.be.true;
    });

    it('should throw error for duplicate email', async () => {
      const userData = {
        email: 'existing@example.com',
        name: 'Test User',
        password: 'password123',
      };

      const error = new Error('Email already exists');
      error.code = 11000;
      userModelStub.create.rejects(error);

      try {
        await userService.createUser(userData);
        expect.fail('Should have thrown an error');
      } catch (err) {
        expect(err.message).to.equal('Email already exists');
      }
    });
  });
});

2. Integration Testing

// tests/integration/orderFlow.test.js
const request = require('supertest');
const { expect } = require('chai');
const app = require('../../api-gateway/server');

describe('Order Flow Integration', () => {
  let authToken;
  let userId;

  before(async () => {
    // Setup test user and get auth token
    const userResponse = await request(app).post('/api/users').send({
      email: 'test@example.com',
      name: 'Test User',
      password: 'password123',
    });

    userId = userResponse.body.id;

    const loginResponse = await request(app).post('/api/auth/login').send({
      email: 'test@example.com',
      password: 'password123',
    });

    authToken = loginResponse.body.token;
  });

  it('should complete order creation flow', async () => {
    // 1. Create order
    const orderResponse = await request(app)
      .post('/api/orders')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        items: [
          {
            productId: 'product-1',
            quantity: 2,
            price: 29.99,
            name: 'Test Product',
          },
        ],
        shippingAddress: {
          street: '123 Test St',
          city: 'Test City',
          country: 'US',
          zipCode: '12345',
        },
      });

    expect(orderResponse.status).to.equal(201);
    expect(orderResponse.body).to.have.property('id');
    expect(orderResponse.body.status).to.equal('pending');

    const orderId = orderResponse.body.id;

    // 2. Process payment
    const paymentResponse = await request(app)
      .post('/api/payments')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        orderId,
        amount: 59.98,
        paymentMethod: 'credit_card',
        cardDetails: {
          number: '4111111111111111',
          expiryMonth: 12,
          expiryYear: 2025,
          cvv: '123',
        },
      });

    expect(paymentResponse.status).to.equal(200);
    expect(paymentResponse.body.status).to.equal('completed');

    // 3. Verify order status updated
    const updatedOrderResponse = await request(app)
      .get(`/api/orders/${orderId}`)
      .set('Authorization', `Bearer ${authToken}`);

    expect(updatedOrderResponse.status).to.equal(200);
    expect(updatedOrderResponse.body.status).to.equal('paid');
  });
});

3. Contract Testing

// tests/contracts/userService.contract.js
const { Pact } = require('@pact-foundation/pact');
const { expect } = require('chai');
const axios = require('axios');

describe('User Service Contract', () => {
  const provider = new Pact({
    consumer: 'order-service',
    provider: 'user-service',
    port: 1234,
  });

  before(() => provider.setup());
  after(() => provider.finalize());
  afterEach(() => provider.verify());

  it('should get user by ID', async () => {
    const userResponse = {
      id: '123',
      email: 'test@example.com',
      name: 'Test User',
    };

    await provider
      .given('user with ID 123 exists')
      .uponReceiving('a request for user 123')
      .withRequest({
        method: 'GET',
        path: '/users/123',
        headers: {
          Accept: 'application/json',
        },
      })
      .willRespondWith({
        status: 200,
        headers: {
          'Content-Type': 'application/json',
        },
        body: userResponse,
      });

    const response = await axios.get('http://localhost:1234/users/123', {
      headers: { Accept: 'application/json' },
    });

    expect(response.data).to.deep.equal(userResponse);
  });
});

Best Practices Summary

1. Design Principles

  • Single Responsibility: Each service should have one business responsibility
  • Loose Coupling: Services should be independent and communicate through well-defined interfaces
  • High Cohesion: Related functionality should be grouped together within a service
  • Autonomous Teams: Each service should be owned by a small, autonomous team

2. Communication Guidelines

  • Prefer Async: Use asynchronous communication when possible to improve resilience
  • Idempotency: Ensure operations can be safely retried
  • Timeouts: Always set reasonable timeouts for external calls
  • Graceful Degradation: Implement fallback mechanisms for service failures

3. Data Management

  • Database per Service: Each service should own its data
  • Eventual Consistency: Accept eventual consistency for better performance and availability
  • Event Sourcing: Consider event sourcing for audit trails and complex business logic
  • CQRS: Separate read and write models when appropriate

4. Security Considerations

  • Zero Trust: Never trust data from other services without validation
  • Encryption: Encrypt data in transit and at rest
  • Authentication: Implement proper service-to-service authentication
  • Authorization: Use fine-grained authorization controls

5. Operational Excellence

  • Observability: Implement comprehensive logging, metrics, and tracing
  • Health Checks: Provide detailed health check endpoints
  • Graceful Shutdown: Handle shutdown signals properly
  • Resource Limits: Set appropriate resource limits and requests

Conclusion

Microservices architecture with Node.js offers powerful capabilities for building scalable, maintainable applications. Success depends on careful design, proper implementation of communication patterns, and adherence to best practices around data management, security, and operational excellence.

Key takeaways:

  1. Start Simple: Begin with a modular monolith and extract services as needed
  2. Focus on Business Capabilities: Design services around business domains
  3. Embrace Async Communication: Use events and messaging for loose coupling
  4. Invest in Tooling: Proper monitoring, logging, and deployment tools are essential
  5. Team Ownership: Align service ownership with team boundaries

Remember that microservices introduce complexity and should only be adopted when the benefits outweigh the costs. Consider your team's expertise, organizational structure, and application requirements when making architectural decisions.

The patterns and practices outlined in this guide provide a solid foundation for building robust microservices architectures with Node.js. Adapt them to your specific use cases and continue learning from the broader microservices community.