¿Cómo funcionan las transacciones distribuidas entre microservicios? — recorre el patrón saga, orquestación vs. coreografía
Cuando una operación lógica abarca cuatro bases de datos que no controlas en conjunto, la atomicidad cede paso a una secuencia de commits locales más acciones compensatorias cuando algo falla más adelante.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo funcionan las transacciones distribuidas entre microservicios? — recorre el patrón saga, orquestación vs. coreografía
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Por qué "usa una transacción y ya" deja de funcionar
Una transacción relacional te da atomicidad porque un único motor controla el log, los bloqueos y el punto de commit. En el momento en que "el pedido", "el pago" y "el conteo de inventario" viven en tres bases de datos distintas, propiedad de tres servicios distintos, ya no existe un único log donde escribir un registro de commit. No puedes hacer BEGIN/COMMIT a través de una frontera de red entre servicios que no controlas conjuntamente — e incluso donde técnicamente podrías (commit de dos fases), no escala: mantiene bloqueos durante un viaje de ida y vuelta por la red y se bloquea indefinidamente si el coordinador muere a mitad del protocolo (la lección hermana sobre 2PC explica exactamente por qué). Así que los sistemas distribuidos cambian atomicidad por consistencia eventual lograda mediante compensación, y el patrón que formaliza esto es la saga.
#Qué es realmente una saga
Una saga es una secuencia de transacciones locales, T1 hasta Tn, donde cada Ti hace commit por completo dentro de un solo servicio (usando la transacción ACID normal de ese servicio contra su propia base de datos) y luego dispara el siguiente paso. Si todas las Ti tienen éxito, la saga se completa y el sistema termina consistente. Si alguna Tk falla, la saga no hace rollback como una transacción de base de datos — en su lugar ejecuta transacciones compensatorias C(k-1), C(k-2), ... C1 para cada paso que ya hizo commit, en orden inverso. Una transacción compensatoria es una operación de negocio nueva, que avanza hacia adelante y deshace semánticamente el efecto de un paso anterior — "emitir un reembolso", no "borrar la fila del pago" — porque la transacción original ya hizo commit de forma duradera y otras transacciones pudieron haberla observado.
#Orquestación vs. coreografía
La orquestación pone a un único coordinador a cargo: conoce de antemano todos los pasos de la saga, llama explícitamente a la transacción local de cada participante (normalmente mediante un comando, por ejemplo una llamada HTTP o un mensaje de comando), espera el resultado y —ante un fallo— llama a la acción compensatoria de cada participante ya completado, en orden inverso. La lógica del flujo vive en un único lugar, lo que la hace fácil de leer, probar y monitorear ("¿dónde se atascó la saga #4471?" tiene un lugar obvio dónde mirar), al costo de que ese orquestador es una pieza nueva de infraestructura que debe conocer la API de cada participante.
La coreografía no tiene coordinador. Cada servicio reacciona a eventos publicados por otros: ejecuta su transacción local y luego publica un evento de dominio describiendo lo que ocurrió; otros servicios se suscriben a los eventos que les interesan y deciden, de forma independiente, si ejecutan su propio siguiente paso o su propia compensación. Esto evita el acoplamiento central de la orquestación y permite añadir servicios sin modificar un flujo central, pero el proceso de negocio en su conjunto ya no está escrito en ningún lado — es el resultado emergente de las suscripciones independientes de N servicios, lo que hace considerablemente más difícil responder "¿qué pasa, en orden, cuando falla un pago?" sin dibujar tú mismo el diagrama, y convierte las dependencias cíclicas entre manejadores de eventos en un riesgo real si dos servicios terminan dependiendo implícitamente de las reacciones del otro.
#El ejemplo concreto que vamos a implementar
Una saga de creación de pedido a través de cuatro servicios:
- OrderService crea el pedido en estado
PENDING. - PaymentService cobra la tarjeta del cliente.
- InventoryService reserva stock para las líneas del pedido.
- ShippingService programa el envío y marca el pedido como
CONFIRMED.
Si la reserva de inventario falla (digamos, una condición de carrera vendió la última unidad), la saga debe compensar: reembolsar el pago y cancelar el pedido. Nunca se llega a shipping, así que no necesita compensación.
#Un orquestador, en código
El orquestador de abajo modela cada paso como un par de funciones asíncronas —execute y compensate— y las ejecuta en orden, deshaciendo lo hecho ante el primer fallo. Esta es la forma que subyace a los frameworks de saga reales (workflows de Temporal, AWS Step Functions, el gestor de sagas de Axon Framework, o una versión artesanal como esta).
class SagaStep {constructor(name, execute, compensate) {this.name = name;this.execute = execute;this.compensate = compensate;}}class SagaOrchestrator {constructor(steps) {this.steps = steps;}async run(context) {const completed = [];for (const step of this.steps) {try {context[step.name] = await step.execute(context);completed.push(step);} catch (err) {console.error('Falló el paso de la saga: ' + step.name + ' -> ' + err.message);await this.compensate(completed, context);throw new Error('Saga abortada en el paso ' + step.name);}}return context;}async compensate(completedSteps, context) {for (let i = completedSteps.length - 1; i >= 0; i--) {const step = completedSteps[i];try {await step.compensate(context);} catch (compErr) {// Una compensación fallida necesita su propia vía de alerta/reintento --// ignorarla en silencio dejaría el sistema inconsistente.console.error('La compensación FALLÓ en el paso ' + step.name + ': ' + compErr.message);}}}}const placeOrderSaga = new SagaOrchestrator([new SagaStep('order',async (ctx) => orderService.create(ctx.customerId, ctx.items),async (ctx) => orderService.cancel(ctx.order.orderId)),new SagaStep('payment',async (ctx) => paymentService.charge(ctx.order.orderId, ctx.amount),async (ctx) => paymentService.refund(ctx.payment.paymentId)),new SagaStep('inventory',async (ctx) => inventoryService.reserve(ctx.order.orderId, ctx.items),async (ctx) => inventoryService.release(ctx.order.orderId, ctx.items)),new SagaStep('shipment',async (ctx) => shippingService.schedule(ctx.order.orderId),async (ctx) => shippingService.cancelSchedule(ctx.order.orderId)),]);// placeOrderSaga.run({ customerId: 'c-1', items: [...], amount: 4999 })// lanza una excepción y deja el sistema consistente si falla la reserva de// inventario, porque el pago y el pedido se compensan en orden inverso.
#El mismo fallo, coreografiado
Sin coordinador, InventoryService escucha el evento que significa "me toca a mí" y publica un evento describiendo su propio resultado; PaymentService escucha el evento de fallo para compensarse a sí mismo:
// InventoryService: reacciona a PaymentCompleted, publica su propio resultadofun onPaymentCompleted(event: PaymentCompletedEvent) {val reserved = inventoryRepository.tryReserve(event.orderId, event.items)if (reserved) {eventBus.publish(InventoryReservedEvent(event.orderId))} else {eventBus.publish(InventoryReservationFailedEvent(event.orderId, event.paymentId))}}// PaymentService: reacciona al fallo compensándose a sí mismo --// nadie se lo ordenó; lo decidió a partir de un evento al que se suscribió.fun onInventoryReservationFailed(event: InventoryReservationFailedEvent) {paymentRepository.refund(event.paymentId)eventBus.publish(PaymentRefundedEvent(event.orderId))}
Fíjate en lo que falta comparado con el orquestador: nada en este archivo sabe que la saga tiene cuatro pasos, ni en qué orden se ejecutan, ni que OrderService también necesita enterarse del fallo para cancelarse a sí mismo (eso es un tercer manejador, en otro lugar, suscrito al mismo evento). Ese conocimiento ahora es implícito en la suma de todas las suscripciones a eventos repartidas por el código — que es exactamente la contrapartida que asume la coreografía.
#La idempotencia no es opcional
Los brokers de mensajería que respaldan las sagas generalmente garantizan entrega al menos una vez, así que cualquiera de los manejadores de arriba puede invocarse dos veces para el mismo evento (un mensaje reentregado tras un ack lento, un comando HTTP reintentado tras un timeout que en realidad sí tuvo éxito del lado del servidor). Si chargeCard no es idempotente, un comando reintentado cobra al cliente dos veces. La solución estándar es una clave de idempotencia: quien llama adjunta un identificador único (a menudo el id de la instancia de saga más el nombre del paso) y el manejador guarda los identificadores ya procesados, cortando en seco una repetición antes de que vuelva a tocar dinero o inventario.
| Orquestación | Coreografía | |
|---|---|---|
| Dónde está definido el flujo | En un solo lugar (el orquestador) | En ningún lado explícitamente — emerge de N suscripciones |
| Acoplamiento | El orquestador conoce la API de cada participante | Los servicios solo conocen los esquemas de eventos, no entre sí |
| Facilidad de depuración | Fácil trazar una instancia de saga de principio a fin | Requiere trazado distribuido para reconstruir un flujo |
| Añadir un paso nuevo | Añadir un paso al orquestador | Añadir un nuevo suscriptor; los servicios existentes no se tocan |
| Riesgo al crecer | El orquestador se vuelve un objeto todopoderoso | Ciclos implícitos entre manejadores de eventos |
:::key Transacción compensatoria, con precisión No es un rollback. Una transacción compensatoria es una operación nueva, con commit independiente, que revierte semánticamente el efecto de negocio de un paso ya confirmado — emitir un reembolso, liberar una reserva, cancelar un envío — porque la escritura original ya es duradera y puede que otras transacciones ya la hayan observado. :::
:::warning La entrega al menos una vez garantiza que tus manejadores VERÁN duplicados Cada participante de la saga debe ser idempotente ante comandos reintentados y eventos reentregados, o el camino de recuperación de fallos de la saga se convierte en una segunda fuente de bugs, encima del fallo que se suponía debía manejar. :::
:::analogy Piénsalo como una "pila de deshacer", no como un "rollback" Un rollback de base de datos borra algo que nunca debió contar. La compensación de una saga se parece más a la pila de deshacer de un editor: la acción original de verdad ocurrió y fue visible, así que deshacerla es en sí misma una acción nueva y registrada — por eso las compensaciones deben diseñarse por operación de negocio, no generarse automáticamente. :::
🧠 Check your understanding
0/3 · 0/3 answered1. En la saga de creación de pedido, InventoryService falla al reservar stock después de que PaymentService ya cobró al cliente. ¿Qué debería pasar a continuación en una saga orquestada?
2. ¿Cuál es la principal desventaja práctica de la coreografía frente a la orquestación?
3. ¿Por qué el manejador de comandos/eventos de cada participante de la saga debe ser idempotente?