Volver a artículos
Desarrollo de Software

Arquitectura de Microservicios con Node.js

Domina patrones de microservicios: comunicación entre servicios, API Gateway, manejo de transacciones distribuidas, Docker y deployment en producción.

Academia Tooldata
30 de agosto de 2026
5 min de lectura
microserviciosnodejsdockerarquitecturaapi-gateway

Introducción a Microservicios

Microservicios es un patrón arquitectónico donde una aplicación se construye como conjunto de servicios pequeños, independientes y desplegables de forma autónoma, cada uno ejecutándose en su propio proceso y comunicándose mediante mecanismos ligeros (generalmente HTTP/REST o mensajería).

Monolito vs Microservicios

Monolito:

┌─────────────────────────────────┐
│                                 │
│    APLICACIÓN MONOLÍTICA        │
│                                 │
│  ┌──────────┐  ┌─────────────┐ │
│  │   Auth   │  │   Products  │ │
│  └──────────┘  └─────────────┘ │
│  ┌──────────┐  ┌─────────────┐ │
│  │  Orders  │  │   Payments  │ │
│  └──────────┘  └─────────────┘ │
│                                 │
│         Base de Datos           │
└─────────────────────────────────┘

Microservicios:

┌───────────────┐   ┌───────────────┐   ┌───────────────┐
│ Auth Service  │   │Product Service│   │ Order Service │
│               │   │               │   │               │
│    DB Auth    │   │   DB Product  │   │   DB Orders   │
└───────────────┘   └───────────────┘   └───────────────┘
        │                   │                   │
        └───────────────────┴───────────────────┘
                            │
                    ┌───────────────┐
                    │  API Gateway  │
                    └───────────────┘

Cuándo Usar Microservicios

✅ Usar microservicios cuando:

  • Equipo grande (>20 developers)
  • Dominios de negocio claramente separados
  • Necesitas escalar componentes independientemente
  • Equipos autónomos con diferentes tecnologías
  • Alto nivel de madurez DevOps

❌ NO usar microservicios cuando:

  • Startup/MVP en fase temprana
  • Equipo pequeño (<5 developers)
  • Dominios de negocio no claros
  • Infraestructura y DevOps limitados
  • Preferir time-to-market rápido

Regla de oro: Empieza con monolito bien modularizado. Migra a microservicios cuando el dolor justifique la complejidad.

Patrones de Comunicación

1. Comunicación Síncrona (HTTP/REST)

// services/order-service/src/index.js
import express from 'express';
import axios from 'axios';

const app = express();
app.use(express.json());

const PRODUCT_SERVICE_URL = process.env.PRODUCT_SERVICE_URL || 'http://product-service:3001';
const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://user-service:3002';

app.post('/orders', async (req, res) => {
  try {
    const { userId, productId, quantity } = req.body;

    // 1. Validar usuario
    const userResponse = await axios.get(`${USER_SERVICE_URL}/users/${userId}`);
    const user = userResponse.data;

    if (!user) {
      return res.status(404).json({ error: 'Usuario no encontrado' });
    }

    // 2. Verificar disponibilidad de producto
    const productResponse = await axios.get(`${PRODUCT_SERVICE_URL}/products/${productId}`);
    const product = productResponse.data;

    if (product.stock < quantity) {
      return res.status(400).json({ error: 'Stock insuficiente' });
    }

    // 3. Crear orden
    const order = {
      id: generateId(),
      userId,
      productId,
      quantity,
      total: product.price * quantity,
      status: 'pending',
      createdAt: new Date()
    };

    await db.orders.create(order);

    // 4. Reducir stock (llamada a product-service)
    await axios.patch(`${PRODUCT_SERVICE_URL}/products/${productId}/stock`, {
      decrement: quantity
    });

    res.status(201).json(order);
  } catch (error) {
    console.error('Error creating order:', error);
    res.status(500).json({ error: 'Error interno del servidor' });
  }
});

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

Problema: ¿Qué pasa si falla la reducción de stock después de crear la orden?

2. Comunicación Asíncrona (Message Queue)

// services/order-service/src/index.js
import express from 'express';
import amqp from 'amqplib';

const app = express();
let channel;

// Conectar a RabbitMQ
async function connectRabbitMQ() {
  const connection = await amqp.connect(process.env.RABBITMQ_URL || 'amqp://localhost');
  channel = await connection.createChannel();

  await channel.assertExchange('orders', 'topic', { durable: true });
  await channel.assertQueue('order.created', { durable: true });

  console.log('Connected to RabbitMQ');
}

connectRabbitMQ();

app.post('/orders', async (req, res) => {
  try {
    const { userId, productId, quantity } = req.body;

    // 1. Crear orden
    const order = {
      id: generateId(),
      userId,
      productId,
      quantity,
      status: 'pending',
      createdAt: new Date()
    };

    await db.orders.create(order);

    // 2. Publicar evento (fire and forget)
    channel.publish(
      'orders',
      'order.created',
      Buffer.from(JSON.stringify(order)),
      { persistent: true }
    );

    res.status(201).json(order);
  } catch (error) {
    console.error('Error creating order:', error);
    res.status(500).json({ error: 'Error interno del servidor' });
  }
});

app.listen(3000);
// services/inventory-service/src/consumer.js
import amqp from 'amqplib';

async function startConsumer() {
  const connection = await amqp.connect(process.env.RABBITMQ_URL);
  const channel = await connection.createChannel();

  await channel.assertExchange('orders', 'topic', { durable: true });
  const queue = await channel.assertQueue('inventory.order.created', { durable: true });

  await channel.bindQueue(queue.queue, 'orders', 'order.created');

  console.log('Inventory Service waiting for order.created events...');

  channel.consume(queue.queue, async (msg) => {
    if (msg) {
      const order = JSON.parse(msg.content.toString());
      console.log('Received order:', order.id);

      try {
        // Reducir stock
        await db.products.update(
          { id: order.productId },
          { $inc: { stock: -order.quantity } }
        );

        console.log(`Stock reduced for product ${order.productId}`);

        // Confirmar procesamiento
        channel.ack(msg);

        // Publicar evento de confirmación
        channel.publish(
          'orders',
          'inventory.reserved',
          Buffer.from(JSON.stringify({
            orderId: order.id,
            productId: order.productId,
            quantity: order.quantity
          }))
        );
      } catch (error) {
        console.error('Error processing order:', error);
        // Reencolar mensaje
        channel.nack(msg, false, true);
      }
    }
  });
}

startConsumer();

Ventajas:

  • Desacoplamiento total
  • Resiliencia (si un servicio está caído, los mensajes se encolan)
  • Escalabilidad independiente

Patrón Saga para Transacciones Distribuidas

Problema: Transacciones en Microservicios

En monolito:

// Todo en una transacción
db.transaction(async (trx) => {
  await trx.orders.create(order);
  await trx.inventory.decrement(productId, quantity);
  await trx.payments.charge(userId, total);
});
// Si algo falla, todo hace rollback

En microservicios: NO hay transacciones distribuidas nativas. Solución: Pattern Saga.

Saga Coreografiada (Event-Driven)

// order-service: Paso 1
async function createOrder(orderData) {
  const order = await db.orders.create({
    ...orderData,
    status: 'pending'
  });

  publishEvent('order.created', order);
  return order;
}

// inventory-service: Paso 2
subscribeToEvent('order.created', async (order) => {
  try {
    await reserveInventory(order.productId, order.quantity);
    publishEvent('inventory.reserved', { orderId: order.id });
  } catch (error) {
    publishEvent('inventory.reservation.failed', {
      orderId: order.id,
      reason: error.message
    });
  }
});

// payment-service: Paso 3
subscribeToEvent('inventory.reserved', async (data) => {
  try {
    await chargePayment(data.orderId);
    publishEvent('payment.completed', { orderId: data.orderId });
  } catch (error) {
    publishEvent('payment.failed', {
      orderId: data.orderId,
      reason: error.message
    });
  }
});

// order-service: Compensación si falla payment
subscribeToEvent('payment.failed', async (data) => {
  await db.orders.update(
    { id: data.orderId },
    { status: 'cancelled', cancelReason: data.reason }
  );

  // Trigger compensación en inventory
  publishEvent('order.cancelled', { orderId: data.orderId });
});

// inventory-service: Compensación
subscribeToEvent('order.cancelled', async (data) => {
  const order = await getOrder(data.orderId);
  await releaseInventory(order.productId, order.quantity);
  publishEvent('inventory.released', { orderId: data.orderId });
});

Saga Orquestada (Orchestrator)

// services/order-orchestrator/src/saga.js
class OrderSaga {
  constructor(orderId) {
    this.orderId = orderId;
    this.steps = [
      { name: 'reserve_inventory', service: 'inventory-service', compensate: 'release_inventory' },
      { name: 'process_payment', service: 'payment-service', compensate: 'refund_payment' },
      { name: 'notify_user', service: 'notification-service', compensate: null }
    ];
    this.completedSteps = [];
  }

  async execute() {
    try {
      for (const step of this.steps) {
        console.log(`Executing step: ${step.name}`);

        const result = await this.executeStep(step);

        if (!result.success) {
          throw new Error(`Step ${step.name} failed: ${result.error}`);
        }

        this.completedSteps.push(step);
      }

      // Saga completada exitosamente
      await this.markOrderComplete();
      return { success: true };

    } catch (error) {
      console.error('Saga failed, initiating compensation:', error);
      await this.compensate();
      return { success: false, error: error.message };
    }
  }

  async executeStep(step) {
    try {
      const response = await axios.post(
        `http://${step.service}/${step.name}`,
        { orderId: this.orderId },
        { timeout: 5000 }
      );
      return { success: true, data: response.data };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  async compensate() {
    // Compensar en orden inverso
    for (const step of this.completedSteps.reverse()) {
      if (step.compensate) {
        console.log(`Compensating: ${step.compensate}`);
        try {
          await axios.post(
            `http://${step.service}/${step.compensate}`,
            { orderId: this.orderId }
          );
        } catch (error) {
          console.error(`Compensation failed for ${step.name}:`, error);
          // Log to dead letter queue para revisión manual
        }
      }
    }

    await this.markOrderFailed();
  }

  async markOrderComplete() {
    await db.orders.update(
      { id: this.orderId },
      { status: 'completed' }
    );
  }

  async markOrderFailed() {
    await db.orders.update(
      { id: this.orderId },
      { status: 'failed' }
    );
  }
}

// Endpoint
app.post('/orders', async (req, res) => {
  const order = await createOrder(req.body);
  const saga = new OrderSaga(order.id);

  const result = await saga.execute();

  if (result.success) {
    res.status(201).json({ orderId: order.id, status: 'completed' });
  } else {
    res.status(400).json({ orderId: order.id, status: 'failed', error: result.error });
  }
});

API Gateway

¿Por Qué API Gateway?

Sin gateway:

Mobile App ──┬──> Auth Service
             ├──> Product Service
             ├──> Order Service
             └──> Payment Service

Problemas:

  • Cliente necesita conocer múltiples endpoints
  • CORS por todos lados
  • Autenticación en cada servicio
  • Sin rate limiting centralizado

Con gateway:

Mobile App ──> API Gateway ──┬──> Auth Service
                             ├──> Product Service
                             ├──> Order Service
                             └──> Payment Service

Implementación con Express

// api-gateway/src/index.js
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
import jwt from 'jsonwebtoken';
import rateLimit from 'express-rate-limit';

const app = express();

// Middleware de autenticación
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

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

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

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100, // límite de 100 requests por ventana
  message: 'Demasiadas peticiones, intenta más tarde'
});

app.use(limiter);

// Rutas públicas (sin autenticación)
app.use('/api/auth', createProxyMiddleware({
  target: 'http://auth-service:3001',
  changeOrigin: true,
  pathRewrite: { '^/api/auth': '' }
}));

// Rutas protegidas
app.use('/api/products', authenticateToken, createProxyMiddleware({
  target: 'http://product-service:3002',
  changeOrigin: true,
  pathRewrite: { '^/api/products': '' },
  onProxyReq: (proxyReq, req) => {
    // Inyectar user info en headers para servicios downstream
    proxyReq.setHeader('X-User-Id', req.user.id);
    proxyReq.setHeader('X-User-Role', req.user.role);
  }
}));

app.use('/api/orders', authenticateToken, createProxyMiddleware({
  target: 'http://order-service:3003',
  changeOrigin: true,
  pathRewrite: { '^/api/orders': '' },
  onProxyReq: (proxyReq, req) => {
    proxyReq.setHeader('X-User-Id', req.user.id);
  }
}));

// Agregación de datos (Backend for Frontend pattern)
app.get('/api/dashboard', authenticateToken, async (req, res) => {
  try {
    const [userResponse, ordersResponse, productsResponse] = await Promise.all([
      axios.get(`http://user-service:3001/users/${req.user.id}`),
      axios.get(`http://order-service:3003/orders?userId=${req.user.id}`),
      axios.get(`http://product-service:3002/products/recommended?userId=${req.user.id}`)
    ]);

    res.json({
      user: userResponse.data,
      recentOrders: ordersResponse.data.slice(0, 5),
      recommendedProducts: productsResponse.data
    });
  } catch (error) {
    console.error('Dashboard aggregation error:', error);
    res.status(500).json({ error: 'Error loading dashboard' });
  }
});

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date() });
});

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

Service Discovery y Load Balancing

Problema: Hardcoded URLs

// ❌ Frágil
const PRODUCT_SERVICE = 'http://product-service:3002';

// ¿Qué pasa si el servicio se mueve?
// ¿Cómo balanc ear carga entre múltiples instancias?

Solución: Service Discovery con Consul

// shared/service-discovery.js
import Consul from 'consul';

const consul = new Consul({
  host: process.env.CONSUL_HOST || 'consul',
  port: process.env.CONSUL_PORT || 8500
});

// Registrar servicio
export async function registerService(name, port, healthCheckPath = '/health') {
  const serviceId = `${name}-${process.env.HOSTNAME || 'local'}`;

  await consul.agent.service.register({
    id: serviceId,
    name: name,
    address: process.env.SERVICE_IP || 'localhost',
    port: port,
    check: {
      http: `http://${process.env.SERVICE_IP}:${port}${healthCheckPath}`,
      interval: '10s',
      timeout: '5s'
    }
  });

  console.log(`Service ${name} registered with Consul`);

  // Deregister on exit
  process.on('SIGINT', async () => {
    await consul.agent.service.deregister(serviceId);
    process.exit(0);
  });
}

// Descubrir servicio
export async function discoverService(name) {
  const result = await consul.health.service({
    service: name,
    passing: true // Solo servicios healthy
  });

  if (result.length === 0) {
    throw new Error(`No healthy instances of ${name} found`);
  }

  // Round-robin simple
  const instance = result[Math.floor(Math.random() * result.length)];

  return {
    host: instance.Service.Address,
    port: instance.Service.Port,
    url: `http://${instance.Service.Address}:${instance.Service.Port}`
  };
}

// Wrapper para axios con service discovery
export async function callService(serviceName, path, options = {}) {
  const service = await discoverService(serviceName);
  const url = `${service.url}${path}`;

  return axios({
    url,
    ...options
  });
}

Uso:

// En cualquier servicio
import { registerService, callService } from './shared/service-discovery.js';

// Al iniciar
await registerService('product-service', 3002);

// Al llamar a otros servicios
const products = await callService('product-service', '/products', {
  method: 'GET',
  params: { category: 'electronics' }
});

Containerización con Docker

Dockerfile para Servicio Node.js

# services/product-service/Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app

# Copiar package files
COPY package*.json ./

# Instalar dependencias
RUN npm ci --only=production

# Copiar código
COPY . .

# Multi-stage para imagen más pequeña
FROM node:18-alpine

WORKDIR /app

# Copiar solo lo necesario desde builder
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/src ./src

# Usuario no-root por seguridad
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

USER nodejs

EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"

CMD ["node", "src/index.js"]

Docker Compose para Desarrollo

# docker-compose.yml
version: '3.8'

services:
  # API Gateway
  api-gateway:
    build:
      context: ./services/api-gateway
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - JWT_SECRET=your-secret-key
      - AUTH_SERVICE_URL=http://auth-service:3001
      - PRODUCT_SERVICE_URL=http://product-service:3002
      - ORDER_SERVICE_URL=http://order-service:3003
    depends_on:
      - auth-service
      - product-service
      - order-service
    networks:
      - microservices-network

  # Auth Service
  auth-service:
    build:
      context: ./services/auth-service
      dockerfile: Dockerfile
    ports:
      - "3001:3001"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://user:password@postgres:5432/auth_db
      - JWT_SECRET=your-secret-key
    depends_on:
      - postgres
      - redis
    networks:
      - microservices-network

  # Product Service
  product-service:
    build:
      context: ./services/product-service
      dockerfile: Dockerfile
    ports:
      - "3002:3002"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://user:password@postgres:5432/product_db
    depends_on:
      - postgres
    networks:
      - microservices-network

  # Order Service
  order-service:
    build:
      context: ./services/order-service
      dockerfile: Dockerfile
    ports:
      - "3003:3003"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://user:password@postgres:5432/order_db
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - postgres
      - rabbitmq
    networks:
      - microservices-network

  # PostgreSQL
  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init-databases.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - microservices-network

  # Redis (para caching)
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    networks:
      - microservices-network

  # RabbitMQ (para messaging)
  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "5672:5672"
      - "15672:15672"  # Management UI
    environment:
      - RABBITMQ_DEFAULT_USER=admin
      - RABBITMQ_DEFAULT_PASS=admin
    networks:
      - microservices-network

volumes:
  postgres-data:

networks:
  microservices-network:
    driver: bridge
-- init-databases.sql
CREATE DATABASE auth_db;
CREATE DATABASE product_db;
CREATE DATABASE order_db;

Comandos:

# Levantar todos los servicios
docker-compose up -d

# Ver logs
docker-compose logs -f product-service

# Escalar servicio
docker-compose up -d --scale product-service=3

# Detener todo
docker-compose down

# Rebuild
docker-compose up -d --build

Observabilidad

Logging Centralizado

// shared/logger.js
import winston from 'winston';
import ecsFormat from '@elastic/ecs-winston-format';

const logger = winston.createLogger({
  format: ecsFormat({ convertReqRes: true }),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error'
    }),
    new winston.transports.File({
      filename: 'logs/combined.log'
    })
  ]
});

// Middleware para request logging
export const requestLogger = (req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;

    logger.info('HTTP Request', {
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration,
      userAgent: req.get('user-agent'),
      ip: req.ip,
      userId: req.user?.id
    });
  });

  next();
};

export default logger;

Distributed Tracing con OpenTelemetry

// shared/tracing.js
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

export function initTracing(serviceName) {
  const provider = new NodeTracerProvider({
    resource: new Resource({
      [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
    }),
  });

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

  provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
  provider.register();

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

  console.log(`Tracing initialized for ${serviceName}`);
}

// En cada servicio
import { initTracing } from './shared/tracing.js';
initTracing('product-service');

Métricas con Prometheus

// shared/metrics.js
import client from 'prom-client';

const register = new client.Registry();

// Métricas default (memoria, CPU, etc.)
client.collectDefaultMetrics({ register });

// Métricas custom
export const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.1, 0.5, 1, 2, 5]
});

export const httpRequestTotal = new client.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code']
});

export const activeConnections = new client.Gauge({
  name: 'active_connections',
  help: 'Number of active connections'
});

register.registerMetric(httpRequestDuration);
register.registerMetric(httpRequestTotal);
register.registerMetric(activeConnections);

// Middleware
export const metricsMiddleware = (req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;

    httpRequestDuration.observe(
      {
        method: req.method,
        route: req.route?.path || req.path,
        status_code: res.statusCode
      },
      duration
    );

    httpRequestTotal.inc({
      method: req.method,
      route: req.route?.path || req.path,
      status_code: res.statusCode
    });
  });

  next();
};

// Endpoint de métricas
export async function metricsEndpoint(req, res) {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
}

Deployment en Kubernetes

Deployment y Service

# k8s/product-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service
  labels:
    app: product-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: product-service
  template:
    metadata:
      labels:
        app: product-service
    spec:
      containers:
      - name: product-service
        image: myregistry/product-service:latest
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: "production"
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: product-service-secrets
              key: database-url
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5

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

Horizontal Pod Autoscaler

# k8s/product-service-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: product-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: product-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Mejores Prácticas

1. Database per Service

❌ Compartir base de datos
Service A ──┐
           └──> Shared DB
Service B ──┘

✅ Base de datos por servicio
Service A ──> DB A
Service B ──> DB B

2. Circuit Breaker Pattern

import CircuitBreaker from 'opossum';

const options = {
  timeout: 3000,
  errorThresholdPercentage: 50,
  resetTimeout: 30000
};

const breaker = new CircuitBreaker(async (productId) => {
  return await axios.get(`http://product-service/products/${productId}`);
}, options);

breaker.fallback(() => ({
  id: null,
  name: 'Producto no disponible',
  cached: true
}));

breaker.on('open', () => {
  logger.warn('Circuit breaker opened for product-service');
});

// Uso
try {
  const product = await breaker.fire(productId);
} catch (error) {
  // Fallback already applied
}

3. API Versioning

// v1
app.use('/api/v1/products', productsV1Router);

// v2 con cambios breaking
app.use('/api/v2/products', productsV2Router);

// Deprecation headers
app.use('/api/v1/*', (req, res, next) => {
  res.setHeader('X-API-Deprecated', 'true');
  res.setHeader('X-API-Sunset', '2025-12-31');
  next();
});

4. Graceful Shutdown

const server = app.listen(3000);

let isShuttingDown = false;

const gracefulShutdown = async () => {
  if (isShuttingDown) return;
  isShuttingDown = true;

  console.log('Received shutdown signal, closing server gracefully...');

  // Dejar de aceptar nuevas conexiones
  server.close(async () => {
    console.log('HTTP server closed');

    try {
      // Cerrar conexiones a bases de datos
      await db.close();
      console.log('Database connections closed');

      // Cerrar conexiones a message queues
      await rabbitmq.close();
      console.log('RabbitMQ connection closed');

      process.exit(0);
    } catch (error) {
      console.error('Error during shutdown:', error);
      process.exit(1);
    }
  });

  // Force shutdown después de 30 segundos
  setTimeout(() => {
    console.error('Forced shutdown after timeout');
    process.exit(1);
  }, 30000);
};

process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);

Próximos Pasos

Esta Semana

  1. Identifica servicios en un proyecto monolítico existente
  2. Implementa un API Gateway simple con Express
  3. Containeriza un servicio con Docker
  4. Configura RabbitMQ para mensajería básica

Próximos 30 Días

  1. Implementa pattern Saga para transacciones distribuidas
  2. Agrega observabilidad (logging, tracing, métricas)
  3. Configura CI/CD para deployment automatizado
  4. Implementa service mesh (Istio/Linkerd)

Recursos Adicionales

  • Libro: "Building Microservices" - Sam Newman
  • Curso: "Microservices with Node.js and React" en Udemy
  • Patterns: microservices.io/patterns
  • Tools: Kubernetes, Istio, Consul, Jaeger

Continúa Aprendiendo

  • Infraestructura: Aprende Kubernetes en profundidad
  • Complementa con: Event-Driven Architecture patterns
  • Monitoreo: Implementa observability stack completo (ELK, Prometheus, Grafana)

Reflexión final: Microservicios añaden complejidad operacional significativa. Asegúrate de que tu organización tiene la madurez técnica y de procesos antes de adoptar esta arquitectura. No es una bala de plata - es un trade-off.

¿Te gustó este artículo?

Explora más contenido educativo en nuestras categorías

Explorar más contenido