El problema
Confirmar un pedido son tres cosas repartidas en tres servicios: reservar el stock, cobrar el pago y avisar al almacén. No hay transacción que abarque las tres, así que hace falta un plan para cuando una falle a mitad.
La saga orquestada resuelve eso con un director: una máquina de estados que conoce los tres pasos, los llama en orden y sabe qué compensar. Funciona muy bien, y tiene un precio que se paga en otra moneda:
- Alguien es dueño del proceso completo. El equipo que mantiene la máquina de estados tiene que entender el negocio de los tres servicios.
- Añadir un participante toca al director. Un cuarto paso —antifraude, por ejemplo— significa que otro equipo modifique y despliegue tu definición.
- El director conoce a todos. Es un punto de acoplamiento que crece con cada proceso nuevo.
En una organización con varios equipos, eso se convierte en una cola de peticiones sobre quien mantiene las máquinas de estados.
La solución
Quitar al director. Cada servicio hace lo suyo, publica el hecho que resulta, y se desentiende. Quien esté interesado, reacciona.
pedidos publica PedidoCreado. stock reacciona, reserva, y publica
StockReservado. pagos reacciona e intenta cobrar; si falla, publica
PagoFallido. Y stock, que también escucha ese evento, libera la reserva.
Lo importante es lo que no hay:
- Nadie llama a nadie. Todo son publicaciones y suscripciones.
- La compensación es otro evento más. Nadie ordena deshacer; alguien reacciona a que algo salió mal.
- Cada servicio conoce solo sus eventos de entrada y de salida, nunca el proceso completo.
Por eso añadir un cuarto participante es gratis para los otros tres: el
antifraude se suscribe a PedidoCreado y ya está. Nadie modifica nada, nadie
despliega nada, nadie se entera.
Cuándo usarlo
- Varios equipos son dueños de sus servicios y nadie debe ser dueño del proceso entero.
- Se espera que aparezcan participantes que hoy no existen.
- Los pasos son razonablemente independientes y la cadena es corta: tres o cuatro eslabones.
- Ya tienes un bus de eventos y la cultura de publicar hechos.
Cuándo NO usarlo
Cómo implementarlo
- Nombra los eventos como hechos pasados y trátalos como contrato público:
PedidoCreado,StockReservado,PagoFallido. Con versión en el payload. - Un identificador de correlación en todos, el mismo de punta a punta. Es lo único que después permitirá reconstruir el proceso.
- Cada servicio publica con outbox, en la misma transacción que su cambio de estado. Sin eso, un eslabón que falla al publicar corta la cadena y nadie se entera.
- Cola SQS entre el bus y cada consumidor, con su DLQ.
- Consumidores idempotentes. Toda la cadena es “al menos una vez”.
- Define los eventos de compensación con el mismo cuidado que los de éxito:
PagoFallidoes tan parte del contrato comoPagoCompletado. - Dibuja la cadena en algún sitio, aunque el código no la contenga. Un diagrama en el repositorio es lo único que va a evitar que nadie entienda el proceso dentro de seis meses.
El código
// ── stock: reacciona a un hecho y publica el suyo ───────────────────
export const reservarStock = async (evento: EventBridgeEvent<'PedidoCreado', Pedido>) => {
const pedido = evento.detail;
// Idempotencia: la entrega es al menos una vez, siempre.
if (await yaReservado(pedido.id)) return;
// Cambiar el estado y encolar el hecho en la MISMA transaccion.
// Es el patron outbox: si no se publica el evento, tampoco se reserva.
await ddb.send(new TransactWriteCommand({
TransactItems: [
{ Put: { TableName: 'stock', Item: reserva(pedido) } },
{ Put: { TableName: 'stock', Item: outbox({
tipo: 'StockReservado',
correlacion: pedido.correlacion, // el hilo de toda la cadena
detalle: { pedidoId: pedido.id, unidades: pedido.unidades },
}) } },
],
}));
};
// ── stock, otra vez: la compensacion es solo otra suscripcion ───────
// Nadie le ordena deshacer. Escucha PagoFallido igual que escuchaba
// PedidoCreado, y actua.
export const liberarStock = async (evento: EventBridgeEvent<'PagoFallido', Fallo>) => {
const { pedidoId } = evento.detail;
if (!(await hayReserva(pedidoId))) return; // idempotente
await liberar(pedidoId);
};
Te va a morder
Coste
| Concepto | Efecto |
|---|---|
| Publicaciones en el bus | ~1,00 USD / millón en EventBridge, ~0,50 en SNS |
| Colas y consumidores | uno por participante y por evento escuchado |
| Transiciones de estado | ninguna — no hay máquina de estados |
| Tiempo de depuración | sube, y no aparece en la factura |
Frente a la orquestación, la coreografía elimina el coste de las transiciones de estado, que en Step Functions Standard es la línea dominante: una saga de seis pasos a un millón de ejecuciones son seis millones de transiciones. Aquí son publicaciones y consumos, que salen bastante más baratos.
Lo que sube es el coste humano, y sube en el peor momento: durante un incidente, cuando hay que reconstruir a mano por dónde iba un proceso que no está escrito en ningún sitio. Ese coste no está en ninguna calculadora y es el que verdaderamente decide entre los dos patrones.
Fuentes
Ausencia de garantías de orden, destinos por regla y throughput por región en
EventBridge:
Amazon EventBridge quotas.
Entrega al menos una vez de SNS y SQS, que obliga a la idempotencia de todos los
eslabones:
Amazon SQS standard queues
y Amazon SNS message delivery retries.
Semántica exactamente-una-vez de Step Functions Standard, usada en la
comparación con la orquestación:
Choosing workflow type in Step Functions.
TransactWriteItems como base del outbox de cada eslabón:
Amazon DynamoDB Transactions.