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:
- Fundamentos y comunicación — modelos de fallo, RPC/gRPC, Kafka, relojes de Lamport.
- Replicación y consistencia — el espectro de la consistencia, CRDTs, conflictos.
- Consenso distribuido — FLP, Raft, quórum, elección de líder.
- Tolerancia a fallos — Circuit Breaker, Bulkhead, split-brain, Chaos Engineering.
- 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.
Solución
| Paso | Evento publicado | Escuchado por | Si falla... | Compensación |
|---|---|---|---|---|
| 1. Pedido confirmado | pedido.confirmado |
Inventario | — | — |
| 2. Inventario reservado | inventario.reservado |
Pagos | Inventario falla | inventario.fallo → Pedidos cancela |
| 3. Pago procesado | pago.procesado |
Delivery | Pago falla | pago.fallo → Inventario libera → Pedidos cancela |
| 4. Repartidor asignado | delivery.asignado |
Notificaciones | Delivery falla | delivery.fallo → Pago reembolsa → Inventario libera → Pedidos cancela |
| 5. Notificación enviada | notificacion.enviada |
— | Reintentar (idempotente) | No hay compensación necesaria |
Nota: Los eventos de fallo activan la cadena de compensaciones en orden inverso.
✏️ 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?
Solución
Recomendación: mantener el monolito. Argumentos:
-
Complejidad operacional: 4 desarrolladores no pueden maintener 10+ servicios con sus infraestructuras, CI/CD pipelines, service meshes, y herramientas de observabilidad.
-
El problema de escala no existe aún: Con 4 devs y MVP, el cuello de botella es el desarrollo de features, no el rendimiento.
-
Premature optimization: Los microservicios son la solución a problemas que tendrás cuando el equipo tenga 50+ personas. Implementarlos ahora paga un costo enorme sin el beneficio.
-
Alternativa: "Monolito modular" — código bien organizado en módulos/paquetes con interfaces claras. Cuando escalen a 20+ devs y el monolito sea un problema real, la migración será más fácil gracias a los módulos bien definidos.
Cuándo reconsiderar: Cuando el time-to-deploy sea > 2 horas, cuando un bug en un módulo derrumbe todo, cuando diferentes partes necesiten escalar 100x diferente.
5.7 Para profundizar
- Kleppmann, DDIA — el libro definitivo sobre sistemas de datos modernos.
- Newman, Building Microservices (2a ed.) — la referencia en microservicios.
- Richardson, Microservices Patterns — patrones como Saga y CQRS.
- Raft paper — raft.github.io
- Kubernetes docs — kubernetes.io/docs
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):
- 3 servicios (cap. 1, 5) —
inventory-api,notification-svc,audit-log. Comunicación con gRPC o REST. - Replicación de la base (cap. 2) — Postgres con una réplica streaming. Demostrá failover manual.
- Saga distribuida (cap. 5) — flujo "registrar venta + descontar inventario + emitir notificación" implementado como Saga con compensación si falla un paso.
- Tolerancia a fallos (cap. 4) — circuit breaker en las llamadas inter-servicio (con
circuitbreakeroresilience4j). Demostrá que cae un servicio y el sistema sigue funcionando degradado. - Observabilidad — logs estructurados con trace ID, métricas Prometheus, dashboard Grafana con latencia p50/p95/p99 por servicio.
- 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.