> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 19. Microservices Architecture

> Design and implement microservices with Node.js, including communication patterns and deployment strategies.

# Microservices Architecture

As applications grow, monolithic architectures become difficult to maintain and scale. Microservices offer a way to build applications as a collection of small, independent services that communicate over the network.

But a word of caution upfront: microservices trade one kind of complexity (a tangled codebase) for another kind (a tangled network). If your team is small or your domain boundaries are unclear, a well-structured monolith will outperform a poorly-designed microservices architecture every time. The right question is not "should we use microservices?" but rather "have we outgrown our monolith?"

## Monolith vs Microservices

| Aspect             | Monolith                | Microservices           |
| ------------------ | ----------------------- | ----------------------- |
| **Deployment**     | All or nothing          | Independent per service |
| **Scaling**        | Scale entire app        | Scale specific services |
| **Technology**     | Single stack            | Best tool for each job  |
| **Team Structure** | Large, coordinated team | Small, autonomous teams |
| **Complexity**     | In the code             | In the infrastructure   |
| **Data**           | Single database         | Database per service    |

### When to Use Microservices

✅ **Use when:**

* Large team (50+ developers)
* Different scaling needs per feature
* Need technology diversity
* High availability requirements
* Complex domain with clear boundaries

❌ **Don't use when:**

* Small team or startup MVP
* Simple CRUD application
* No clear service boundaries
* Limited DevOps capabilities

## Service Design Principles

### Single Responsibility

Each service should do one thing well.

```
✅ Good Service Boundaries:
- User Service (auth, profiles)
- Order Service (cart, checkout)
- Payment Service (transactions)
- Notification Service (email, SMS, push)
- Inventory Service (stock management)

❌ Poor Boundaries:
- User and Order Service (too coupled)
- Everything Service (just a monolith)
```

### API Gateway Pattern

In a microservices architecture, clients should never call individual services directly. Instead, they talk to a single entry point -- the API Gateway -- which routes requests to the appropriate backend service. This is like a hotel concierge: guests make all their requests through one person, who knows which department handles what.

The gateway also handles cross-cutting concerns that would otherwise be duplicated in every service: authentication, rate limiting, request logging, and CORS.

```javascript theme={null}
// gateway/app.js
const express = require('express');
const httpProxy = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');

const app = express();

// Rate limiting
app.use(rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
}));

// Authentication middleware
app.use('/api', authMiddleware);

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

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

app.use('/api/products', httpProxy.createProxyMiddleware({
  target: 'http://product-service:3003',
  changeOrigin: true,
  pathRewrite: { '^/api/products': '' }
}));

app.listen(3000);
```

## Service Communication

### Synchronous (HTTP/REST)

```javascript theme={null}
// order-service calling user-service
const axios = require('axios');

class UserServiceClient {
  constructor() {
    this.baseURL = process.env.USER_SERVICE_URL || 'http://user-service:3001';
    this.client = axios.create({
      baseURL: this.baseURL,
      timeout: 5000
    });
  }

  async getUser(userId) {
    try {
      const { data } = await this.client.get(`/users/${userId}`);
      return data;
    } catch (error) {
      if (error.response?.status === 404) {
        return null;
      }
      throw new Error(`User service error: ${error.message}`);
    }
  }

  async validateUser(userId) {
    const user = await this.getUser(userId);
    return user !== null;
  }
}

module.exports = new UserServiceClient();
```

### Circuit Breaker Pattern

When Service A depends on Service B, and Service B goes down, naive retries from Service A will pile up, consuming threads and memory until Service A also crashes -- a cascade failure. The circuit breaker pattern prevents this by acting like an electrical circuit breaker: after a threshold of failures, it "opens" and immediately returns a fallback response without even attempting the network call. After a cooldown period, it allows a test request through to see if the downstream service has recovered.

There are three states: **Closed** (normal operation, requests pass through), **Open** (service is down, requests fail fast), and **Half-Open** (testing if the service has recovered).

```javascript theme={null}
const CircuitBreaker = require('opossum');

const options = {
  timeout: 3000,       // If a single call takes longer than 3s, count it as a failure
  errorThresholdPercentage: 50, // If 50% of requests fail, open the circuit
  resetTimeout: 30000  // After 30s, try one request to check if the service recovered
};

const breaker = new CircuitBreaker(async (userId) => {
  const response = await axios.get(`${USER_SERVICE_URL}/users/${userId}`);
  return response.data;
}, options);

breaker.on('open', () => console.log('Circuit breaker opened'));
breaker.on('halfOpen', () => console.log('Circuit breaker half-open'));
breaker.on('close', () => console.log('Circuit breaker closed'));

// Usage
const getUser = async (userId) => {
  try {
    return await breaker.fire(userId);
  } catch (error) {
    // Return cached data or default
    return { id: userId, name: 'Unknown (service unavailable)' };
  }
};
```

### Asynchronous (Message Queue)

Synchronous HTTP calls between services create tight coupling: if the downstream service is slow or down, the caller blocks or fails. Message queues decouple services by introducing a buffer between them. The producer drops a message into the queue and moves on immediately; the consumer processes it whenever it is ready. This is like leaving a note on someone's desk instead of waiting for them at their office door.

Message queues also provide durability (messages survive a service restart), load leveling (consumers process at their own pace), and fan-out (multiple consumers can process the same event type).

```bash theme={null}
npm install amqplib
```

```javascript theme={null}
// RabbitMQ message producer/consumer
const amqp = require('amqplib');

class MessageQueue {
  async connect() {
    this.connection = await amqp.connect(process.env.RABBITMQ_URL);
    this.channel = await this.connection.createChannel();
  }

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

  async subscribe(queue, handler) {
    await this.channel.assertQueue(queue, { durable: true });
    this.channel.consume(queue, async (msg) => {
      try {
        const content = JSON.parse(msg.content.toString());
        await handler(content);
        this.channel.ack(msg);
      } catch (error) {
        console.error('Message handling error:', error);
        this.channel.nack(msg, false, false); // Dead letter
      }
    });
  }
}

// Order service - publish event
const orderCreated = async (order) => {
  await mq.publish('order.created', {
    orderId: order.id,
    userId: order.userId,
    items: order.items,
    total: order.total,
    timestamp: new Date().toISOString()
  });
};

// Notification service - consume event
mq.subscribe('order.created', async (event) => {
  const user = await userService.getUser(event.userId);
  await sendEmail(user.email, 'Order Confirmation', {
    orderId: event.orderId,
    total: event.total
  });
});

// Inventory service - consume event
mq.subscribe('order.created', async (event) => {
  for (const item of event.items) {
    await decreaseStock(item.productId, item.quantity);
  }
});
```

## Event-Driven Architecture

```javascript theme={null}
// Event types
const EventTypes = {
  USER_CREATED: 'user.created',
  USER_UPDATED: 'user.updated',
  ORDER_CREATED: 'order.created',
  ORDER_PAID: 'order.paid',
  ORDER_SHIPPED: 'order.shipped',
  PAYMENT_RECEIVED: 'payment.received',
  PAYMENT_FAILED: 'payment.failed'
};

// Event structure
const createEvent = (type, data, metadata = {}) => ({
  id: uuid(),
  type,
  data,
  metadata: {
    timestamp: new Date().toISOString(),
    version: '1.0',
    source: process.env.SERVICE_NAME,
    correlationId: metadata.correlationId || uuid(),
    ...metadata
  }
});

// Publish event
await eventBus.publish(createEvent(EventTypes.ORDER_CREATED, {
  orderId: order.id,
  userId: order.userId,
  items: order.items
}));
```

## Service Discovery

### With Docker Compose

```yaml theme={null}
# docker-compose.yml
version: '3.8'

services:
  gateway:
    build: ./gateway
    ports:
      - "3000:3000"
    depends_on:
      - user-service
      - order-service
    environment:
      - USER_SERVICE_URL=http://user-service:3001
      - ORDER_SERVICE_URL=http://order-service:3002

  user-service:
    build: ./user-service
    environment:
      - DATABASE_URL=mongodb://mongo:27017/users
    depends_on:
      - mongo

  order-service:
    build: ./order-service
    environment:
      - DATABASE_URL=postgres://postgres:password@postgres:5432/orders
      - USER_SERVICE_URL=http://user-service:3001
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - postgres
      - rabbitmq

  mongo:
    image: mongo:6

  postgres:
    image: postgres:15
    environment:
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=orders

  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "15672:15672"  # Management UI
```

### With Consul (Production)

```javascript theme={null}
const Consul = require('consul');

const consul = new Consul({
  host: process.env.CONSUL_HOST || 'localhost'
});

// Register service
const registerService = async () => {
  await consul.agent.service.register({
    name: 'user-service',
    id: `user-service-${process.env.HOSTNAME}`,
    address: process.env.HOST_IP,
    port: parseInt(process.env.PORT),
    check: {
      http: `http://${process.env.HOST_IP}:${process.env.PORT}/health`,
      interval: '10s'
    }
  });
};

// Discover service
const getServiceUrl = async (serviceName) => {
  const services = await consul.catalog.service.nodes(serviceName);
  if (services.length === 0) {
    throw new Error(`Service ${serviceName} not found`);
  }
  // Simple round-robin
  const service = services[Math.floor(Math.random() * services.length)];
  return `http://${service.ServiceAddress}:${service.ServicePort}`;
};
```

## Data Management

### Database Per Service

```javascript theme={null}
// User service - MongoDB
const mongoose = require('mongoose');
mongoose.connect(process.env.MONGO_URI);

const UserSchema = new mongoose.Schema({
  email: { type: String, unique: true },
  name: String,
  createdAt: { type: Date, default: Date.now }
});

// Order service - PostgreSQL with Prisma
// prisma/schema.prisma
model Order {
  id        Int      @id @default(autoincrement())
  userId    String   // Reference to user in another service
  items     Json
  total     Decimal
  status    OrderStatus
  createdAt DateTime @default(now())
}

// Product service - Redis for fast access
const redis = require('redis');
const client = redis.createClient(process.env.REDIS_URL);

const getProduct = async (id) => {
  const cached = await client.get(`product:${id}`);
  if (cached) return JSON.parse(cached);
  
  const product = await fetchFromDB(id);
  await client.setEx(`product:${id}`, 3600, JSON.stringify(product));
  return product;
};
```

### Saga Pattern for Distributed Transactions

In a monolith, you wrap multiple database operations in a single transaction -- if anything fails, everything rolls back. In microservices, each service has its own database, so traditional transactions do not work across service boundaries. The saga pattern solves this by breaking a distributed transaction into a sequence of local transactions, each with a compensating action that undoes its work if a later step fails.

Think of it like booking a vacation: you reserve a flight, then a hotel, then a rental car. If the rental car is unavailable, you cancel the hotel and the flight -- each cancellation is a "compensating transaction."

There are two flavors: **choreography** (each service listens for events and reacts, no central coordinator) and **orchestration** (a central saga manager tells each service what to do). Choreography is simpler for small sagas; orchestration is easier to reason about for complex multi-step flows.

```javascript theme={null}
// Choreography-based saga -- each service reacts to events independently
class OrderSaga {
  constructor(eventBus) {
    this.eventBus = eventBus;
    this.setupListeners();
  }

  setupListeners() {
    // Step 1: Order created -> Reserve inventory
    this.eventBus.subscribe('order.created', async (event) => {
      try {
        await this.reserveInventory(event.data);
        await this.eventBus.publish('inventory.reserved', event.data);
      } catch (error) {
        await this.eventBus.publish('inventory.reservation.failed', event.data);
      }
    });

    // Step 2: Inventory reserved -> Process payment
    this.eventBus.subscribe('inventory.reserved', async (event) => {
      try {
        await this.processPayment(event.data);
        await this.eventBus.publish('payment.completed', event.data);
      } catch (error) {
        // Compensate: release inventory
        await this.releaseInventory(event.data);
        await this.eventBus.publish('payment.failed', event.data);
      }
    });

    // Step 3: Payment completed -> Confirm order
    this.eventBus.subscribe('payment.completed', async (event) => {
      await this.confirmOrder(event.data);
      await this.eventBus.publish('order.confirmed', event.data);
    });

    // Compensation handlers
    this.eventBus.subscribe('payment.failed', async (event) => {
      await this.cancelOrder(event.data);
    });
  }
}
```

## Health Checks and Monitoring

```javascript theme={null}
// Health check endpoint
app.get('/health', async (req, res) => {
  const health = {
    status: 'healthy',
    service: process.env.SERVICE_NAME,
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    checks: {}
  };

  // Database check
  try {
    await db.query('SELECT 1');
    health.checks.database = { status: 'healthy' };
  } catch (error) {
    health.status = 'unhealthy';
    health.checks.database = { status: 'unhealthy', error: error.message };
  }

  // Message queue check
  try {
    await mq.checkConnection();
    health.checks.messageQueue = { status: 'healthy' };
  } catch (error) {
    health.status = 'unhealthy';
    health.checks.messageQueue = { status: 'unhealthy', error: error.message };
  }

  // External service check
  try {
    await userService.ping();
    health.checks.userService = { status: 'healthy' };
  } catch (error) {
    health.status = 'degraded'; // Might still work
    health.checks.userService = { status: 'unhealthy', error: error.message };
  }

  const statusCode = health.status === 'healthy' ? 200 : 503;
  res.status(statusCode).json(health);
});
```

## Distributed Tracing

In a monolith, when a request is slow, you look at one set of logs. In microservices, a single user request might flow through 5 different services. Without distributed tracing, debugging "why was this request slow?" becomes a needle-in-a-haystack problem across multiple log streams.

Distributed tracing assigns a unique **trace ID** to each incoming request and propagates it through every service the request touches. Each service records **spans** (timed operations), and tracing tools like Jaeger or Zipkin stitch them together into a visual timeline showing exactly where time was spent.

```javascript theme={null}
const { trace } = require('@opentelemetry/api');

// Trace an operation
const tracer = trace.getTracer('order-service');

app.post('/orders', async (req, res) => {
  const span = tracer.startSpan('createOrder');
  
  try {
    span.setAttribute('userId', req.body.userId);
    
    // Create child span for each operation
    const userSpan = tracer.startSpan('validateUser', { parent: span });
    const user = await userService.getUser(req.body.userId);
    userSpan.end();
    
    const orderSpan = tracer.startSpan('saveOrder', { parent: span });
    const order = await Order.create(req.body);
    orderSpan.end();
    
    span.setAttribute('orderId', order.id);
    res.json(order);
  } catch (error) {
    span.recordException(error);
    span.setStatus({ code: 2, message: error.message });
    throw error;
  } finally {
    span.end();
  }
});
```

## Summary

* **Start with a monolith** - Extract services when needed
* **Define clear boundaries** - One service, one responsibility
* **Use API Gateway** - Single entry point for clients
* **Implement circuit breakers** - Handle service failures gracefully
* **Prefer async communication** - Message queues reduce coupling
* **Database per service** - Avoid shared databases
* **Handle distributed transactions** - Use saga pattern
* **Monitor everything** - Health checks, tracing, logging
* **Container orchestration** - Use Kubernetes in production
