Arquitecturas modernas de sistemas distribuidos

"Los microservicios no son la solución a todos los problemas — son la solución a los problemas que surgen cuando el monolito se vuelve inmanejable. Hasta ese punto, el monolito es mejor."

Qué vas a aprender en este capítulo

Los sistemas modernos a gran escala — Netflix, Uber, Amazon — usan microservicios. Pero microservicios introducen nuevos problemas: transacciones distribuidas, coordinación de servicios, observabilidad. Este capítulo explora las soluciones modernas a estos problemas.


5.1 Microservicios vs Monolito

💡 Intuición

Un monolito es un sistema donde todo el código vive en un solo proceso. Un microservicio es un sistema descompuesto en múltiples procesos independientes, cada uno con su propia base de datos.

La Esquina empezó como un monolito (un solo servidor Flask + una BD PostgreSQL). Al crecer a nivel nacional con millones de pedidos, algunos componentes (el sistema de delivery, los pagos) necesitan escalar de forma independiente — ahí nacen los microservicios.

📐 Fundamento

Cuándo migrar a microservicios:

El "monolito modular" es una alternativa frecuentemente ignorada: mantener un solo proceso pero con módulos bien separados. Antes de microservicios, verificar que:

  • El equipo tiene > 50 ingenieros (microservicios requieren mucha infraestructura).
  • Partes del sistema tienen requisitos de escala drásticamente diferentes.
  • Partes del sistema necesitan desplegarse de forma independiente.
  • El monolito tiene deuda técnica tan grande que refactorizar es más difícil que reescribir.

Arquitectura de La Esquina Cloud:

Cliente (App Móvil)
       │
       ▼
  [API Gateway]  ← enrutamiento, autenticación, rate limiting
       │
  ┌────┴────┬──────────┬──────────┐
  ▼         ▼          ▼          ▼
[Pedidos]  [Menú]  [Pagos]   [Delivery]
  │BD         │BD     │BD         │BD
  │           │       │           │
  └───────────┴───────┴─────┬─────┘
                             │
                        [Kafka]   ← event bus
                             │
                      [Notificaciones]
                      [Analíticas]
                      [Reportes]

Database per service: Cada microservicio tiene su propia base de datos, independiente de los demás. Garantiza el desacoplamiento pero complica las transacciones que cruzan servicios.

API Gateway: Punto único de entrada que:

  • Enruta requests al servicio correcto.
  • Gestiona autenticación y autorización.
  • Implementa rate limiting.
  • Agrega múltiples responses si es necesario (API composition).
# Configuración nginx como API Gateway básico
upstream pedidos_service {
    server pedidos:8001;
}
upstream menu_service {
    server menu:8002;
}

server {
    listen 80;
    
    location /api/pedidos {
        proxy_pass http://pedidos_service;
    }
    location /api/menu {
        proxy_pass http://menu_service;
    }
}

5.2 El patrón Saga para transacciones distribuidas

💡 Intuición

En un monolito, "crear un pedido" es una transacción ACID: atómica, o todo funciona o nada. En microservicios, "crear un pedido" involucra: el servicio de Pedidos, el servicio de Inventario, el servicio de Pagos. No hay un BEGIN TRANSACTION que abarque tres bases de datos independientes.

Saga es el patrón para transacciones distribuidas: una secuencia de transacciones locales, donde cada una publica un evento para disparar la siguiente. Si algo falla, se ejecutan transacciones compensatorias (el equivalente del ROLLBACK distribuido).

📐 Fundamento

Saga coreografiada (event-driven):

# Servicio de Pedidos
class PedidoService:
    def crear_pedido(self, pedido_data):
        # Transacción local
        pedido = db.create(pedido_data, estado='PENDIENTE')
        
        # Publicar evento → dispara el siguiente paso
        kafka.publish('pedido.creado', {
            'pedido_id': pedido.id,
            'items': pedido_data['items'],
            'monto': pedido_data['monto']
        })
        return pedido

# Servicio de Inventario — escucha 'pedido.creado'
class InventarioService:
    def on_pedido_creado(self, evento):
        try:
            # Reservar inventario
            for item in evento['items']:
                self.reservar(item['platillo_id'], item['cantidad'])
            
            kafka.publish('inventario.reservado', {'pedido_id': evento['pedido_id']})
        except StockInsuficiente:
            # Transacción compensatoria: cancelar el pedido
            kafka.publish('inventario.fallo', {
                'pedido_id': evento['pedido_id'],
                'motivo': 'Stock insuficiente'
            })

# Servicio de Pedidos — escucha 'inventario.fallo'
class PedidoService:
    def on_inventario_fallo(self, evento):
        # Transacción compensatoria: marcar pedido como cancelado
        db.update(evento['pedido_id'], estado='CANCELADO')
        notificar_cliente(evento['pedido_id'], 'Pedido cancelado por falta de stock')

# Flujo completo:
# pedido.creado → inventario.reservado → pago.procesado → pedido.confirmado
#                 → inventario.fallo → pedido.cancelado (compensación)

Saga orquestada (con orquestador central):

Un servicio orquestador coordina explícitamente los pasos:

class SagaCrearPedido:
    """Orquestador que coordina los pasos de crear un pedido."""
    
    def ejecutar(self, pedido_id):
        try:
            # Paso 1
            pedido = self.pedido_service.confirmar_pedido(pedido_id)
            
            # Paso 2
            try:
                self.inventario_service.reservar(pedido.items)
            except StockInsuficiente as e:
                self.pedido_service.cancelar(pedido_id, str(e))
                return {'exito': False, 'motivo': str(e)}
            
            # Paso 3
            try:
                self.pago_service.cobrar(pedido.monto, pedido.metodo_pago)
            except PagoFallido as e:
                self.inventario_service.liberar(pedido.items)  # compensación
                self.pedido_service.cancelar(pedido_id, str(e))
                return {'exito': False, 'motivo': str(e)}
            
            # Todo exitoso
            self.pedido_service.confirmar_listo(pedido_id)
            return {'exito': True}
        
        except Exception as e:
            # Error inesperado — ejecutar compensaciones en orden inverso
            self._compensar(pedido_id)
            raise

Coreografía vs Orquestación:

Coreografiada Orquestada
Coordinación Eventos (sin punto central) Orquestador central
Acoplamiento Bajo Medio (depende del orquestador)
Visibilidad Difícil (flujo disperso) Fácil (flujo en un lugar)
Tolerancia a fallos Alta (no hay SPOF) El orquestador puede fallar

5.3 Service Mesh

💡 Intuición

Con 20 microservicios, cada uno necesita: TLS entre servicios, retry logic, circuit breakers, métricas de red, distributed tracing. ¿Los implementamos 20 veces, uno en cada servicio?

Un service mesh (malla de servicios) extrae toda esa lógica a un sidecar proxy que se inyecta junto a cada servicio. El servicio solo habla con su sidecar local (localhost) — el sidecar maneja todo lo demás.

📐 Fundamento

Arquitectura de service mesh con Istio/Envoy:

[Servicio Pedidos] ←→ [Envoy Proxy]  ←  mTLS  →  [Envoy Proxy] ←→ [Servicio Menú]
                           ↕                               ↕
                    [Control Plane (Istio)]  ←  configura  →  [Control Plane]

Lo que el sidecar maneja automáticamente:

# VirtualService: traffic management
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: pedidos-service
spec:
  hosts:
    - pedidos-service
  http:
    - route:
      - destination:
          host: pedidos-service
          subset: v1
        weight: 90           # 90% del tráfico a v1
      - destination:
          host: pedidos-service
          subset: v2
        weight: 10           # 10% a v2 (canary deployment)
      retries:
        attempts: 3
        perTryTimeout: 2s
      timeout: 10s

# DestinationRule: circuit breaker
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: pedidos-cb
spec:
  host: pedidos-service
  trafficPolicy:
    outlierDetection:
      consecutiveErrors: 5   # 5 errores consecutivos → expulsar del pool
      interval: 10s
      baseEjectionTime: 30s

Kubernetes — orquestador de contenedores:

# Deployment de microservicio de pedidos
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pedidos-service
spec:
  replicas: 3              # 3 instancias para alta disponibilidad
  selector:
    matchLabels:
      app: pedidos
  template:
    spec:
      containers:
        - name: pedidos
          image: esquina/pedidos:v2.1.0
          ports:
            - containerPort: 8001
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          livenessProbe:   # Kubernetes mata y reinicia si falla
            httpGet:
              path: /health
              port: 8001
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:  # Kubernetes no envía tráfico hasta que esté listo
            httpGet:
              path: /ready
              port: 8001

5.4 Event-Driven Architecture y CQRS

📐 Fundamento

CQRS (Command Query Responsibility Segregation):

Separar el modelo de escritura (Command) del modelo de lectura (Query). Las escrituras van a la BD normalizada; las lecturas van a proyecciones desnormalizadas optimizadas para consultas.

[App] → [Command Handler] → [Write DB (PostgreSQL normalizado)]
                                      ↓
                               [Event Store] → [Projector]
                                                    ↓
                               [Read DB (Redis/ElasticSearch desnormalizado)]
                                      ↑
                              [Query Handler] ← [App]

Event Sourcing:

En lugar de guardar el estado actual, guardar todos los eventos que llevaron a ese estado. El estado se reconstruye reproduciendo los eventos.

# Event sourcing para pedidos
EVENTOS = [
    {'tipo': 'PedidoCreado',     'timestamp': '2026-05-05T12:00:00', 'mesa_id': 3},
    {'tipo': 'ItemAgregado',     'timestamp': '2026-05-05T12:01:00', 'platillo_id': 1, 'cantidad': 2},
    {'tipo': 'PedidoConfirmado', 'timestamp': '2026-05-05T12:01:30'},
    {'tipo': 'PedidoListo',      'timestamp': '2026-05-05T12:15:00'},
    {'tipo': 'PedidoCerrado',    'timestamp': '2026-05-05T12:30:00', 'total': 8.50},
]

def reconstruir_pedido(eventos: list) -> dict:
    estado = {}
    for evento in eventos:
        match evento['tipo']:
            case 'PedidoCreado':
                estado = {'mesa_id': evento['mesa_id'], 'items': [], 'estado': 'ABIERTO'}
            case 'ItemAgregado':
                estado['items'].append({'platillo_id': evento['platillo_id'], 
                                         'cantidad': evento['cantidad']})
            case 'PedidoConfirmado':
                estado['estado'] = 'CONFIRMADO'
            case 'PedidoListo':
                estado['estado'] = 'LISTO'
            case 'PedidoCerrado':
                estado['estado'] = 'CERRADO'
                estado['total'] = evento['total']
    return estado

pedido_actual = reconstruir_pedido(EVENTOS)

Ventajas del Event Sourcing:

  • Auditoría completa — sabés exactamente qué pasó y cuándo.
  • Reproducir el estado en cualquier punto del pasado.
  • Fácil de agregar nuevas proyecciones (analytics, reportes) sin modificar el core.

Desventajas:

  • Mayor complejidad.
  • Las queries de estado actual requieren reproducir todos los eventos (mitigado con snapshots).

🛠️ En la práctica

La Esquina Cloud: arquitectura final

┌─────────────────────────────────────────────────────────────┐
│                       Clientes                              │
│          App Móvil │ Web │ Tablet Cocina                    │
└─────────────────────────────────────────────────────────────┘
                              │
                     [Cloudflare CDN]
                              │
                    [API Gateway (Kong)]
                    Auth │ Rate Limiting │ Routing
                              │
         ┌────────────────────┼──────────────────────┐
         │                    │                       │
   [Pedidos MS]         [Menú MS]            [Pagos MS]
   PostgreSQL           Redis+PG             PostgreSQL
   (por local)          (read-heavy)         (strictACID)
         │                    │
         └────────┬───────────┘
                  │
           [Kafka Cluster]
           3 brokers, replicado
                  │
    ┌─────────────┼──────────────┐
    │             │              │
[Delivery MS] [Notif MS]  [Analytics MS]
[PostgreSQL]  [Firebase]  [ClickHouse]

Infraestructura:
- Kubernetes en AWS (EKS), 3 zonas de disponibilidad
- Istio service mesh
- Prometheus + Grafana + Jaeger (observabilidad)
- Raft (etcd) para coordinación del clúster

Esta arquitectura puede manejar:

  • 10,000 pedidos/hora distribuidos entre todos los locales.
  • Failover automático en < 30 segundos si cae un nodo.
  • Zero-downtime deployments con rolling updates en Kubernetes.
  • Consistencia fuerte para pedidos y pagos; eventual para analytics.

5.5 Cierre del libro

Este libro recorrió el universo de los sistemas distribuidos:

  1. Fundamentos y comunicación — modelos de fallo, RPC/gRPC, Kafka, relojes de Lamport.
  2. Replicación y consistencia — el espectro de la consistencia, CRDTs, conflictos.
  3. Consenso distribuido — FLP, Raft, quórum, elección de líder.
  4. Tolerancia a fallos — Circuit Breaker, Bulkhead, split-brain, Chaos Engineering.
  5. Arquitecturas modernas — microservicios, Saga, service mesh, CQRS, Kubernetes.

Los sistemas distribuidos son difíciles porque las fallas son inevitables, la red es no confiable, y los relojes no están sincronizados. La ingeniería consiste en diseñar sistemas que fallen bien — que se degraden gracefully y se recuperen automáticamente.


5.6 Ejercicios

✏️ Ejercicio 5.1 — Diseño de Saga

Diseñá una Saga coreografiada para el proceso de delivery de La Esquina Cloud:

Pasos: (1) Cliente confirma pedido → (2) Inventario reserva los platillos → (3) Pagos cobra → (4) Delivery asigna repartidor → (5) Notificaciones informa al cliente.

Para cada paso: indicá el evento publicado, quién lo escucha, y la transacción compensatoria si falla.

✏️ Ejercicio 5.2 — Monolito vs Microservicios

Una startup de software tiene 4 desarrolladores y acaba de lanzar su MVP. El CTO propone migrar a microservicios "para escalar mejor". ¿Qué le recomendarías?


5.7 Para profundizar

5.X Mini-proyecto integrador

🏗️ Proyecto final — Sistema distribuido tolerante a fallos

Alcance: diseñar y construir un sistema de 3-4 servicios que aplique los conceptos del libro: replicación, consenso simulado, tolerancia a fallos y observabilidad.

Idea sugerida: un sistema de inventario distribuido para una cadena de pupuserías. Cada local tiene su instancia local; al final del día se reconcilia con central.

Entregables (un repo con docker-compose):

  1. 3 servicios (cap. 1, 5) — inventory-api, notification-svc, audit-log. Comunicación con gRPC o REST.
  2. Replicación de la base (cap. 2) — Postgres con una réplica streaming. Demostrá failover manual.
  3. Saga distribuida (cap. 5) — flujo "registrar venta + descontar inventario + emitir notificación" implementado como Saga con compensación si falla un paso.
  4. Tolerancia a fallos (cap. 4) — circuit breaker en las llamadas inter-servicio (con circuitbreaker o resilience4j). Demostrá que cae un servicio y el sistema sigue funcionando degradado.
  5. Observabilidad — logs estructurados con trace ID, métricas Prometheus, dashboard Grafana con latencia p50/p95/p99 por servicio.
  6. Test de caos — un script que mata un container random cada 2 minutos durante 10 min. Verificá que el sistema se recupera solo.

Criterio de éxito: podés explicar, mostrando los dashboards, qué pasó durante el test de caos.

Tiempo estimado: 4-6 semanas. Es el proyecto de portafolio top para roles backend / SRE.


Definiciones nuevas: monolito, microservicio, API Gateway, database-per-service, Saga, transacción compensatoria, coreografía, orquestación, service mesh, sidecar proxy, Istio, Envoy, Kubernetes, CQRS, Event Sourcing, projector, snapshot, ClickHouse, zero-downtime deployment.