En pocas palabras
Secuencia de transacciones locales en distintos servicios donde cada paso, si algo falla después, tiene una acción de compensación que deshace su efecto semántico.
Qué problema resuelve
Mantener la consistencia de un proceso que cruza varias bases de datos sin recurrir a transacciones distribuidas (2PC), que bloquean y no escalan.
Cuándo utilizarlo
Procesos largos que tocan varios servicios. Ejemplo: aprobar un crédito, reservar fondos, firmar el pagaré y desembolsar.
De la idea a la práctica
Funcionamiento, ejemplos y decisiones para pasar al siguiente nivel.
Coreografía frente a orquestación
| Aspecto | Coreografía | Orquestación |
|---|---|---|
| Coordinación | Cada servicio reacciona a eventos de otros | Un orquestador envía comandos y espera respuestas |
| Visibilidad | El flujo está repartido; difícil de ver entero | El flujo está en un sitio; fácil de seguir y auditar |
| Acoplamiento | Bajo entre servicios, alto con los eventos | Los participantes no se conocen entre sí |
| Adecuado para | 2–4 pasos estables | Procesos largos, con plazos, ramas o intervención humana |
Ejemplo: desembolso orquestado
| Paso | Servicio | Acción | Compensación |
|---|---|---|---|
| 1 | Crédito | Marcar EN_DESEMBOLSO |
Volver a APROBADO |
| 2 | Tesorería | Reservar fondos | Liberar reserva |
| 3 | Firma | Firmar pagaré digital | Anular pagaré |
| 4 | Pagos | Transferir al cliente | — (paso pivote, no compensable) |
| 5 | Contabilidad | Registrar asiento | Reintentar hasta completar |
El paso pivote es el punto de no retorno. Antes de él todo se compensa; después, los pasos deben ser reintentables hasta terminar. Ordena la saga para que lo arriesgado y validable ocurra antes del pivote.
Implementación con un motor de workflows
Escribir una máquina de estados con tablas, temporizadores y reintentos es costoso. Motores como Temporal, Camunda o AWS Step Functions persisten el estado de la saga y reanudan tras fallos. Un esbozo con el SDK Java de Temporal:
public void desembolsar(String creditoId) {
Saga saga = new Saga(new Saga.Options.Builder().build());
try {
credito.marcarEnDesembolso(creditoId);
saga.addCompensation(credito::volverAAprobado, creditoId);
String reserva = tesoreria.reservar(creditoId);
saga.addCompensation(tesoreria::liberar, reserva);
firma.firmarPagare(creditoId);
saga.addCompensation(firma::anular, creditoId);
pagos.transferir(creditoId); // pivote
} catch (ActivityFailure e) {
saga.compensate();
throw e;
}
contabilidad.registrar(creditoId); // reintentos según su RetryOptions
}
Aislamiento: el punto débil
Las sagas no ofrecen la «I» de ACID. Otro proceso puede leer datos intermedios. Contramedidas habituales:
- Bloqueo semántico: estados como
EN_DESEMBOLSOque impiden otras operaciones. - Versionado u optimistic locking sobre el agregado.
- Releer el valor antes de un paso crítico.
- Orden conmutativo de operaciones cuando sea posible.
Antipatrones frecuentes
- Compensaciones no idempotentes: se ejecutan dos veces y liberan doble reserva. Usa idempotencia.
- Coreografía con diez servicios: nadie sabe en qué estado está un crédito.
- Ignorar los plazos: una saga «en curso» indefinidamente necesita timeouts y alertas.
Orquestador propio con tabla de estado
Cuando no se adopta un motor de workflows, el estado de cada saga se persiste explícitamente:
CREATE TABLE saga_desembolso (
saga_id uuid PRIMARY KEY,
credito_id text NOT NULL UNIQUE,
paso_actual text NOT NULL, -- RESERVAR_FONDOS, FIRMAR, TRANSFERIR…
estado text NOT NULL, -- EN_CURSO, COMPENSANDO, COMPLETADA, FALLIDA
intentos int NOT NULL DEFAULT 0,
datos jsonb NOT NULL, -- ids de reserva, pagaré, transferencia
vence_en timestamptz, -- plazo del paso actual
actualizado_en timestamptz NOT NULL DEFAULT now()
);
Un proceso revisa periódicamente las sagas con vence_en pasado para reintentar o compensar. Cada transición se guarda en la misma transacción que publica el comando siguiente (transactional outbox), de modo que un reinicio del orquestador retoma exactamente donde quedó.
Motores de workflow: conceptos clave
| Concepto | Significado práctico |
|---|---|
| Workflow determinista | El código de orquestación se re-ejecuta tras fallos; no puede leer la hora o generar aleatorios directamente |
| Actividad | Paso con efectos (llamar a una API); tiene su propia política de reintentos y timeouts |
| Señales y consultas | Recibir eventos externos (aprobación humana) y consultar el estado en curso |
| Temporizadores duraderos | Esperar días («si el cliente no firma en 72 h, anular») sin ocupar recursos |
| Versionado | Cambiar el código sin romper instancias en curso |
Observabilidad de sagas
- Panel con instancias por estado y paso, y antigüedad de las que están en curso.
- Alertas por sagas que superan su plazo o entran en
COMPENSANDO. - Correlación por
credito_iden logs y trazas de todos los participantes. - Vista para soporte: «¿en qué paso está el desembolso del cliente 42 y por qué?».
Probar las compensaciones
| Escenario | Verificación |
|---|---|
| Falla el paso 3 (firma) | Se liberan los fondos y el crédito vuelve a APROBADO |
| La compensación falla una vez | Se reintenta y termina; no queda una reserva huérfana |
| Mensaje duplicado de «fondos reservados» | Una sola reserva |
| El orquestador se reinicia a mitad | Continúa desde el último paso registrado |
| El pivote se ejecuta y falla después | Los pasos posteriores se reintentan hasta completar |
Con Temporal, el entorno de pruebas permite saltar el tiempo y simular fallos de actividades de forma determinista.
Criterios de implementación
Modela la saga con negocio, incluyendo qué se comunica al cliente en cada fallo. Expón el estado de cada instancia para soporte y auditoría. Publica los eventos de cada paso con transactional outbox y prueba explícitamente cada ruta de compensación.
Continúa en las fuentes
Documentación oficial para verificar detalles y explorar más.