Saga Pattern
Cómo manejar transacciones distribuidas entre microservicios usando coreografía u orquestación con compensaciones.
Qué problema resuelve
En un monolito, una transacción de base de datos puede abarcar múltiples tablas con un simple BEGIN → COMMIT → ROLLBACK. Pero en una arquitectura de microservicios, cada servicio tiene su propia base de datos. No existe una transacción global que pueda hacer rollback en 5 bases de datos diferentes si algo falla en el paso 3.
El Saga Pattern resuelve este problema dividiendo una transacción distribuida en una secuencia de transacciones locales, donde cada paso tiene una acción compensatoria que deshace su efecto si un paso posterior falla.
Ejemplo del problema
Imagina un flujo de compra en un e-commerce:
- Servicio de Pedidos: Crear pedido
- Servicio de Inventario: Reservar stock
- Servicio de Pagos: Cobrar al cliente
- Servicio de Envíos: Programar envío
Si el pago falla en el paso 3, necesitas deshacer la reserva de stock (paso 2) y cancelar el pedido (paso 1). Sin un patrón definido, esto se convierte en un caos de llamadas manuales y estados inconsistentes.
Por qué no usar transacciones distribuidas (2PC)
El protocolo Two-Phase Commit (2PC) parece la solución obvia, pero tiene problemas serios en microservicios:
| Aspecto | 2PC | Saga |
|---|---|---|
| Disponibilidad | Todos los participantes deben estar disponibles simultáneamente | Cada paso es independiente |
| Latencias durante toda la transacción | Cada paso libera recursos inmediatamente | |
| Escalabilidad | Los locks distribuidos limitan el throughput | Sin locks, cada servicio escala independientemente |
| Acoplamiento | Requiere un coordinador que conozca a todos | Los servicios se comunican por eventos |
| Recuperación | Si el coordinador falla, los participantes quedan bloqueados | Las compensaciones se ejecutan de forma asíncrona |
Cómo funciona
Una saga es una secuencia de transacciones locales donde:
- Cada transacción actualiza datos dentro de un solo servicio
- Cada transacción publica un evento o mensaje que dispara la siguiente
- Si una transacción falla, se ejecutan compensaciones en orden inverso
Flujo exitoso:
T1 → T2 → T3 → T4 ✅
Flujo con fallo en T3:
T1 → T2 → T3 ❌ → C2 → C1
(C = compensación)
Existen dos estrategias principales para coordinar una saga:
Coreografía (Choreography)
Cada servicio escucha eventos y decide qué hacer. No hay un coordinador central.
┌──────────┐ PedidoCreado ┌──────────────┐
│ Pedidos │ ──────────────────► │ Inventario │
└──────────┘ └──────┬───────┘
│
StockReservado
│
▼
┌──────────────┐
│ Pagos │
└──────┬───────┘
│
PagoAprobado
│
▼
┌──────────────┐
│ Envíos │
└──────────────┘
Ventajas: Sin punto único de fallo, servicios desacoplados, fácil agregar participantes. Desventajas: Difícil entender el flujo completo, riesgo de ciclos, difícil depurar.
Orquestación (Orchestration)
Un componente central coordina toda la saga. Sabe qué pasos ejecutar y qué compensaciones aplicar.
┌──────────────────┐
│ Orquestador │
│ de Saga │
└──┬───┬───┬───┬──┘
│ │ │ │
Crear │ │ │ │ Programar
pedido │ │ │ │ envío
▼ │ │ ▼
┌────────┐│ │┌────────┐
│Pedidos ││ ││Envíos │
└────────┘│ │└────────┘
Reservar │ Cobrar
stock │ │ pago
▼ ▼
┌────────┐┌────────┐
│Invent. ││ Pagos │
└────────┘└────────┘
Ventajas: Flujo centralizado, control explícito de compensaciones, fácil depurar. Desventajas: Punto único de fallo potencial, riesgo de God Service, acoplamiento.
Cuándo elegir cada estrategia
| Cquestación | |---|---|---| | Número de pasos | 2-4 pasos simples | 5+ pasos o flujos complejos | | Visibilidad | Aceptable si es simple | Necesaria para flujos críticos | | Equipos | Autónomos e independientes | Equipo central gestiona el flujo | | Compensaciones | Simples y directas | Complejas con lógica condicional | | Monitoreo | Trazabilidad distribuida | Dashboard centralizado |
Transacciones compensatorias
Las compensaciones no son simplemente un “undo”. Son acciones de o que semánticamente revierten el efecto de una transacción anterior:
| Transacción | Compensación |
|---|---|
| Crear pedido | Cancelar pedido (cambiar estado, no eliminar) |
| Reservar stock | Liberar stock reservado |
| Cobrar pago | Emitir reembolso |
| Programar envío | Cancelar programación de envío |
Puntos clave sobre compensaciones:
- No son rollbacks: No borran datos, crean nuevos registros que revierten el efecto
- Deben ser idempotentes: Ejuce el mismo resultado
- Pueden fallar: Necesitas reintentos para compensaciones fallidas
- Mantienen auditoría: Todo queda registrado (acción original y compensación)
Ejemplo práctico: Saga de compra con orquestación
Camino feliz
Orquestador Pedidos Inventario Pagos Envíos
│ │ │ │ │
│── CrearPedido ───►│ │ │ │
│◄── PedidoCreado ──│ │ │ │
│ │ │ │ │
│── ReservarStock ──────────────►│ │ │
│◄── StockReservado ────────────│ │ │
│ │ │ │ │
│── CobrarPago ─────────────────────────────►│ │
│◄── PagoAprobado ──────────────────────────│ │
│ │ │ │ │
│── ProgramarEnvío ─────────────────────────────────────►│
───────────────────────────│
│ │ │ │ │
✅ Saga completada
Fallo en el pago (con compensaciones)
Orquestador Pedidos Inventario Pagos
│ │ │ │
│── CrearPedido ───►│ │ │
│◄── PedidoCreado ──│ │ │
│ │ │ │
│── ReservarStock ──────────────►│ │
│◄── StockReservado ────────────│ │
│ │ │ │
│── CobrarPago ─────────────────────────────►│
│◄── PagoRechazado ────────────────────────│ ❌
│ │ │ │
│ ┌── COMPENSACIONES ──────────────────┐ │
│ │── LiberarStock ────────────►│ │ │
│ │◄── StockLiberado ──────────│ │ │
│ │── CancelarPedido ──►│ │ │ │
│ │◄── PedidoCancelado ─│ │ │ │
│ └────────────────────────────────────┘ │
│ │ │
❌ Saga compensada
Estado persistido del orquestador
{
"sagaId": "saga-12345",
"tipo": "CompraCompleta",
"estado": "COMPENSANDO",
"pasoActual": "LiberarStock",
"datos": {
"pedidoId": "ped-001",
"clienteId": "cli-789",
"monto": 150.00
},
"historial": [
{ "paso": "CrearPedido", "estado": "COMPLETADO" },
{ "paso": "ReservarStock", "estado": "COMPLETADO" },
cientes" },
{ "paso": "LiberarStock", "estado": "COMPENSANDO" }
]
}
Ventajas
- Consistencia eventual: Garantiza que el sistema llegará a un estado consistente
- Autonomía de servicios: Cada servicio mantiene su propia base de datos y transacciones locales
- Resiliencia: Si un paso falla, las compensaciones restauran la consistencia
- Escalabilidad: No requiere locks distribuidos ni coordinación síncrona
- Auditoría natural: Cada paso y compensación queda registrado como evento
- Flexibilidad: Se pueden agregar o modificar pasos sin afectar todo el flujo
Trade-offs / Desventajas
- Complejidad: Diseñar compensaciones correctas para cada paso no es trivial
- Consistencia eventual: El sistema puede estar temporalmente inconsistente
- Difícil de depurar: Rastrear una saga requiere buenas herramientas de observabilidad
- Compensaciones imperfectas: No siempre es posible revertir completamente una acción
- Orden de ejecución: En coreografía, garantizarrecto es complejo
- Testing: Probar todos los caminos posibles requiere esfuerzo significativo
- Ventana de inconsistencia: Durante la ejecución, los datos pueden estar en estado parcial
Cuándo usar
- Transacciones que cruzan múltiples microservicios con bases de datos independientes
- Flujos de negocio de larga duración (minutos, horas o días)
- Sistemas donde la consistencia eventual es aceptable
- Cuando necesitas auditoría completa de cada paso del proceso
- Flujos con pasos que pueden fallar y necesitan compensación definida
- Sistemas event-driven donde los servicios ya se comunican por eventos
Cuándo evitar
- Operaciones que requieren consistencia fuerte e inmediata (ACID)
- Flujos simples que involucran solo 2 servicios (puede ser overkill)
- Cuando las compensaciones son imposibles o no tienen sentido
- Sistemas donde la ventana de inconsistencia no es aceptable
- Equipos sin experiencia en sistemas distribuidos
- Cuando un monolito con transacciones locales resuelve el problema
as e implementaciones comunes
| Categoría | Opciones |
|---|---|
| Orquestadores | Temporal, Camunda, AWS Step Functions, Netflix Conductor |
| Mensajería | Apache Kafka, RabbitMQ, Amazon SQS/SNS, Azure Service Bus |
| Frameworks | Axon Framework (Java), MassTransit (.NET), Eventuate Tram |
| Observabilidad | Jaeger, Zipkin, OpenTelemetry (tracing distribuido) |
| Estado de saga | PostgreSQL, Redis, DynamoDB |
Relación con otros patrones
:
┌─────────────────────────────────────────────────┐
│ Ecosistema de la Saga │
│ │
│ ┌──────────┐ publica ┌──────────────┐ │
│ │ Saga │ ──────────────►│ Outbox │ │
│ │ (pasos) │ eventos │ (confiable) │ │
│ └────┬─────┘ └──────────────┘ │
│ │ │
│ │ cada paso es │
│ │ idempotente │
│ ▼ │
│ ┌──────────────┐ protege ┌───────────┐ │
│ │ Idempotency │ ◄────────────►│ Circuit │ │
│ │ Pattern │ llamadas │ Breaker │ │
│ └──────────────┘ └───────────┘ │
│ │
│ ┌──────────┐ reintentos ┌───────────┐ │
│ │ Retry │ ◄────────────────►│ Timeout │ │
│ │ Pattern │ con límite │ Pattern │ │
└───────────┘ │
└─────────────────────────────────────────────────┘
- Outbox Pattern: Garantiza que los eventos de cada paso se publiquen de forma confiable
- Idempotency Pattern: Cada paso y compensación debe ser idempotente para reintentos seguros
- Circuit Breaker: Protege al orquestador cuando un servicio participante está caído
- Retry Pattern: Reintenta pasos fallidos con backoff antes de activar compensaciones
- *siderarlo fallido
- Event-Driven Architecture: La saga se construye sobre un sistema de eventos
- DDD: Los bounded contexts definen los límites de cada participante
Errores comunes
| Error | Consecuencia | Solución |
|---|---|---|
| Compensaciones no idempotentes | Duplicación de reembolsos | Usar IDs de idempotencia |
| No persistir estado de la saga | Sagas perdidas tras reinicio | Guardar estado en BD durable |
| Ignorar fallos en compener queue | ||
| Saga con demasiados pasos | Complejidad inmanejable | Dividir en sub-sagas |
| No monitorear sagas activas | Sagas atascadas sin detectar | Dashboard con alertas |
Próximos pasos
Una plora el Idempotency Pattern para asegurar que los pasos de la saga sean seguros ante reintentos.