Módulo 6: Reintentos, alertas y recuperación

3. Acciones compensatorias: deshacer lo que no puedes evitar repetir

Descripción

Al terminar esta lección vas a poder diseñar una acción compensatoria: cuando un efecto ya ocurrió y no puedes hacerlo idempotente ni evitarlo, lo deshaces con una acción opuesta. Vas a entender el patrón "por cada crear, un anular", vas a ver cómo issue-refund se convierte en la compensación de una retención de crédito que quedó huérfana, y vas a aprender por qué un rollback perfecto casi nunca existe en un sistema que toca el mundo real —correos que ya se enviaron, mensajes que ya se mandaron— y qué significa entonces dejar el sistema en un estado aceptable en lugar de idéntico. También vas a ver que la compensación misma tiene que ser idempotente, porque deshacer dos veces puede ser tan dañino como hacer dos veces.

Esto importa porque la lección anterior te dio la mitad de la respuesta al fallo: reintentar, cuando el efecto es idempotente. Pero muchos efectos no se pueden proteger con una clave, o involucran varios pasos que no se pueden hacer de una sola vez de forma atómica. check-credit coloca la retención, e inventory-sync, un paso después, descubre que no hay stock. La retención ya está puesta. No hay una casilla que la "no coloque". La única salida honesta es deshacerla, y eso es una acción compensatoria. Sin este patrón, tu sistema acumula efectos a medias: retenciones que bloquean crédito de pedidos que nunca se surtieron, reservas de inventario de pedidos que se cancelaron. Es la diferencia entre un sistema que se limpia solo y uno que deja basura por todos lados.

Conexión con el módulo: la lección 2 fue "cuando puedes repetir sin daño". Esta es "cuando no puedes, deshaces". Juntas son las dos reacciones posibles al fallo de un efecto. Esta lección se apoya en el patrón outbox del Módulo 5 —vas a ver que decidir compensar y ejecutar la compensación son dos pasos separados, exactamente como en el outbox—, en el ledger del Módulo 4 —donde queda registrado qué se hizo y qué se compensó— y en la idempotencia del Módulo 2, porque la compensación también tiene que ser idempotente. La lección 4 recoge lo que pase después: cuándo una compensación que falla merece una alerta.

Qué es una acción compensatoria

Vamos a definirla bien, porque es un concepto que se usa mal con frecuencia.

Una acción compensatoria es una operación que deshace el efecto de una operación anterior, ejecutándola como un paso normal más del sistema. No es una función mágica de "deshacer" que borra el pasado. Es una acción real, con su propio efecto: si "colocar una retención" fue la operación, "liberar la retención" es su compensación. Si "cobrar" fue la operación, "reembolsar" es su compensación. Por cada acción que crea algo, defines de antemano la acción que lo anula.

La analogía más clara es una reserva de viaje. Reservas un vuelo y pagas. Un minuto después, intentas reservar el hotel para las mismas fechas y no hay disponibilidad. Ya pagaste el vuelo. No puedes viajar al pasado para no haberlo pagado. Lo que hace la agencia es emitir un reembolso del vuelo: una acción nueva, real, que deja tu tarjeta como estaba —o casi—. El reembolso es la acción compensatoria del cobro. No es que el cobro "nunca pasó"; es que hubo un cobro y después un reembolso, y el resultado neto es aceptable.

Fíjate en dos cosas de esa analogía, porque las dos vuelven en todo lo que sigue. Primera: la compensación es una acción hacia adelante, no un retroceso. El sistema no rebobina; hace algo nuevo que contrarresta lo anterior. Segunda: el resultado no es idéntico a "nunca pasó nada". En tu estado de cuenta quedan dos movimientos —un cargo y un abono—, no cero movimientos. Para casi todos los propósitos eso está bien, pero no es lo mismo que si nunca hubieras pagado. Esa diferencia entre aceptable e idéntico es el tema de una sección entera más adelante.

Por qué existe: no todo se puede hacer atómico

Para entender por qué necesitas compensaciones, hay que ver qué es lo que no tienes en un sistema de integración.

En una base de datos, existe algo llamado transacción: agrupas varias escrituras y le dices a la base "o se hacen todas, o no se hace ninguna". Si a la mitad algo falla, la base revierte —hace rollback— y deja todo como estaba, sin rastro. Es atómico: todo o nada. El Módulo 4 tocó esto para el ledger.

El problema es que esa magia solo funciona dentro de una base de datos. Tu sistema de Cumbre no vive dentro de una sola base: coloca una retención llamando a la API de crédito, reserva inventario en otro lado, emite reembolsos por una tercera API. Esos son sistemas distintos, cada uno con su propia base de datos, y no hay ninguna transacción que abarque a los tres. No puedes decirle a la API de crédito y al sistema de inventario "o se hacen las dos cosas o ninguna", porque no se conocen entre sí y no comparten un mecanismo de todo-o-nada.

Piénsalo así: si tres personas distintas tienen que hacer tres favores para que un plan funcione, y no hay un jefe que las coordine, no puedes garantizar que las tres lo hagan o ninguna. Lo que puedes hacer es esto: si la primera hizo su parte y la segunda no pudo, le pides a la primera que deshaga lo que hizo. Eso es una compensación, y es lo mejor que se puede lograr cuando no hay una transacción que abarque a todos.

Este patrón —una secuencia de pasos, cada uno con su compensación, de modo que si algo falla a la mitad se deshacen los pasos ya hechos— tiene nombre en la ingeniería de sistemas: se llama saga. No necesitas el término para trabajar, pero conviene conocerlo, porque es exactamente lo que estás construyendo cuando le pones a check-credit su liberación de retención y a un cobro su reembolso. Una saga es una transacción que no cabe en una sola base de datos, sostenida a mano con compensaciones.

El patrón: por cada crear, un anular

La disciplina concreta es simple de enunciar y da trabajo aplicarla: para cada operación que crea un efecto, define de antemano la operación que lo anula. Antes de escribir el paso que coloca la retención, ya tienes claro cuál es el paso que la libera. No es algo que improvisas cuando falla; es parte del diseño desde el principio.

Para el sistema de Cumbre, la tabla de compensaciones se ve así:

Operación (crea)Compensación (anula)¿Deja rastro?
check-credit coloca un credit_holdissue-refund libera el credit_holdPoco: la línea de crédito queda como estaba
inventory-sync reserva stockUn release que devuelve el stock reservadoPoco: el inventario vuelve a estar disponible
Un cobro real a la tarjeta del clienteissue-refund emite un reembolsoSí: quedan dos movimientos en el estado de cuenta
Un correo de confirmación al clienteNo hay compensación limpiaSí, y es irreversible: el correo ya se leyó

Mira la última fila, porque es la más honesta. Algunas operaciones no tienen compensación limpia. Un correo enviado no se puede "des-enviar": ya llegó, quizás ya se leyó. Un mensaje de WhatsApp de "tu pedido va en camino" no se retira. Para esas operaciones, la mejor compensación posible es otra comunicación —"disculpa, tu pedido se canceló"—, que no deshace la primera sino que la corrige socialmente. Y eso lleva a una decisión de diseño importantísima que veremos en un momento: el orden en que ejecutas los efectos, para que los que no se pueden deshacer ocurran al final.

Ejemplo trabajado: issue-refund como compensación de una retención huérfana

Veamos el flujo completo de Cumbre. El escenario: entra el pedido ORD-3180 de Luna Coffee por 4820 pesos. check-credit coloca la retención con éxito. Un paso después, inventory-sync intenta reservar 20 kilos de café arábica y descubre que solo hay 8. La reserva falla. La retención de 4820 pesos quedó huérfana: bloquea crédito de un pedido que no se va a surtir. Hay que liberarla.

Paso 1 — Registrar qué se hizo, para poder deshacerlo. Aquí entra el ledger del Módulo 4. Cuando check-credit coloca la retención con éxito, escribe en el ledger un registro de que este order_id tiene una retención activa. Sin ese registro, cuando algo falle más adelante no sabrías qué hay que compensar. La compensación necesita saber qué se hizo, y el ledger es donde eso vive.

-- Tabla del ledger, con la columna que rastrea el estado del efecto.
-- (Esquema conceptual; el detalle de la tabla es del Módulo 4.)
run_ledger
  order_id        TEXT
  credit_hold_id  TEXT      -- el id que devolvió la API de crédito
  hold_status     TEXT      -- 'active' | 'released'
  ...

Paso 2 — Detectar que hay que compensar. Cuando inventory-sync no puede reservar el stock, el sistema tiene que decidir: "el pedido no se puede surtir, y hay una retención activa; hay que liberarla". Esta decisión de compensar es un paso separado de la ejecución de la compensación. Y esa separación es exactamente el patrón outbox del Módulo 5: no llamas al reembolso directamente en el momento del fallo; escribes una intención de compensar en la tabla outbox, y un consumidor la ejecuta.

-- Se escribe una intención de compensación, no se ejecuta en caliente.
outbox
  intent_id    TEXT
  intent_type  TEXT      -- 'release_credit_hold'
  order_id     TEXT      -- 'ORD-3180'
  payload      JSONB     -- { credit_hold_id, amount, reason: 'out_of_stock' }
  status       TEXT      -- 'pending' -> 'done'

¿Por qué separarlo? Por la misma razón que en el Módulo 5: si el momento del fallo es caótico —justo cuando algo se rompió— no quieres que la compensación dependa de que todo lo demás funcione en ese instante. Escribes la intención de forma confiable, y la compensación se ejecuta después, con sus propios reintentos, de forma independiente. Si el sistema se cayera entre la decisión y la ejecución, la intención sigue ahí, esperando, y no se pierde.

Paso 3 — Ejecutar la compensación. El workflow issue-refund lee las intenciones pendientes del outbox y, para cada release_credit_hold, llama a la API de crédito para liberar la retención. La llamada es un HTTP Request —recuerda, desde un nodo Code no puedes hacer HTTP en n8n 2.0—, y lleva su clave de idempotencia:

// ============================================================
// Nodo: Code — "Build release key" (dentro de issue-refund)
// Modo: Run Once for Each Item
//
// ENTRADA:  una intención del outbox: release_credit_hold
// SALIDA:   el item con una clave de idempotencia para la liberación
// POR QUÉ:   liberar dos veces la misma retención debe contar como una;
//            la clave lo garantiza aunque este paso se reintente
// ============================================================

const intent = $json;

// La clave se deriva de la intención concreta, no del instante:
// misma intención -> misma clave -> la API libera una sola vez.
const releaseKey = `release-hold:${intent.order_id}:${intent.credit_hold_id}`;

return {
  json: {
    ...intent,
    idempotency_key: releaseKey,
  },
};

El HTTP Request que sigue manda esa clave como header al llamar a la API de liberación, y tiene Retry On Fail activado (lección 2), lo cual es seguro precisamente porque la operación es idempotente.

Paso 4 — Registrar que se compensó. Cuando la liberación tiene éxito, se actualiza el ledger: hold_status pasa de 'active' a 'released', y la intención del outbox pasa a 'done'. Ahora el sistema sabe que este pedido ya no tiene una retención activa, y una futura corrida no va a intentar liberarla otra vez.

Qué esperar del flujo completo. El pedido ORD-3180 entra, coloca una retención, falla al reservar inventario, y en cuestión de segundos —o el tiempo que tarde el consumidor del outbox— la retención se libera automáticamente. Luna Coffee nunca tuvo su crédito bloqueado más que un instante. El ledger cuenta la historia entera: retención colocada, después liberada, motivo "sin stock". Y como todo el flujo es idempotente, si cualquier paso se reintenta —o si el webhook original disparó dos veces— no hay una segunda retención ni una segunda liberación. Ese es un sistema que se limpia solo.

Por qué el rollback perfecto casi nunca existe

Ahora la parte incómoda, y es la que separa a quien entendió el patrón de quien solo lo memorizó.

En una base de datos, un rollback deja las cosas exactamente como estaban: cero rastro. En un sistema de integración con efectos en el mundo real, eso casi nunca es posible, por tres razones que conviene tener claras.

Primera: hay efectos que ya salieron y no se retiran. Si antes de que algo falle mandaste un correo de "pedido confirmado", ese correo ya está en la bandeja del cliente. Puedes mandar otro corrigiéndolo, pero no puedes hacer que el primero no haya existido. Lo mismo con un SMS, un mensaje de WhatsApp, una notificación push. La comunicación es, casi por definición, irreversible.

Segunda: la compensación deja su propio rastro. Un cobro compensado con un reembolso no equivale a "sin cobro". El cliente vio el cargo en su tarjeta, quizás se asustó, y ahora ve también un abono. En su estado de cuenta hay dos líneas, no cero. Para la contabilidad puede dar igual el neto, pero la experiencia no fue neutra. Y en algunos sistemas —comisiones de la pasarela de pago, por ejemplo— un cobro-y-reembolso incluso cuesta dinero real que no se recupera.

Tercera: hay una ventana de tiempo donde el estado es inconsistente. Entre que se coloca la retención y se libera, hay un intervalo —segundos, a veces minutos— en el que el cliente tiene el crédito bloqueado por un pedido que ya se sabe que no se va a surtir. Si en ese preciso instante el cliente intenta otro pedido y su crédito no alcanza por culpa de la retención huérfana, el efecto ya lo tocó. La compensación limpia el estado, pero no borra lo que pasó durante la ventana.

La conclusión práctica no es deprimente, es liberadora: no diseñas para un rollback perfecto, porque no existe. Diseñas para dejar el sistema en un estado aceptable. Un estado aceptable es uno donde no queda dinero atrapado, no queda inventario reservado de más, no queda un cliente cobrado por algo que no recibió —aunque sí queden rastros, movimientos, correos—. La pregunta de diseño no es "¿cómo hago que sea como si nunca hubiera pasado?", que no tiene respuesta. Es "¿cuál es el peor estado en que puede quedar esto, y cómo lo llevo a uno aceptable?".

El orden de los efectos: haz lo irreversible al final

De todo lo anterior sale una regla de diseño concreta y muy útil: cuando tengas varios efectos que ejecutar, pon primero los que son fáciles de deshacer y deja para el final los que son irreversibles.

La razón es directa. Si mandas el correo de confirmación antes de reservar el inventario, y la reserva falla, ya mandaste un correo que ahora tienes que desmentir. Si mandas el correo después de que la reserva tuvo éxito, el correo solo sale cuando ya sabes que el pedido es viable, y nunca tienes que desmentirlo.

En el sistema de Cumbre, el orden ideal es: primero las operaciones reversibles y baratas de deshacer —colocar la retención (se libera), reservar el inventario (se devuelve)—, y solo cuando todas tuvieron éxito, las operaciones irreversibles —el correo al cliente, el cobro definitivo—. Así, si algo se cae, lo que hay que compensar es siempre reversible, y lo irreversible nunca llegó a ocurrir.

Piénsalo como cocinar para invitados: no mandas la invitación "la cena está lista, vengan" hasta que la comida está lista. El orden protege: dejas el paso que no se puede retirar para cuando ya no hay riesgo de tener que retirarlo.

La compensación también tiene que ser idempotente

Un punto que se olvida y muerde: la acción compensatoria es un efecto como cualquier otro, y por lo tanto también se puede duplicar y también hay que protegerla.

Piénsalo. El consumidor del outbox que ejecuta la liberación puede reintentarse (lección 2). El webhook que originó todo pudo disparar dos veces. La intención de compensar podría escribirse dos veces si algo salió raro. En cualquiera de esos casos, si "liberar la retención" no es idempotente, terminas liberando dos veces —y liberar dos veces una retención podría, según cómo esté hecha la API, devolver crédito de más, o dar un error confuso, o dejar el ledger inconsistente—.

Por eso, en el ejemplo trabajado, la liberación llevaba su propia Idempotency-Key derivada del credit_hold_id, y el ledger marcaba hold_status = 'released'. Esas dos cosas juntas garantizan que la liberación ocurra exactamente una vez: la clave protege del lado de la API, y el estado en el ledger protege del lado del sistema —si ya está 'released', no se vuelve a intentar—.

La regla, entonces, es recursiva y limpia: todo lo que aprendiste sobre idempotencia para las operaciones normales aplica igual a las compensaciones. Deshacer no es una excepción a las reglas del módulo; es una operación más que juega con las mismas reglas. Reembolsar dos veces es tan malo como cobrar dos veces.

Cuando ni la compensación alcanza

Vale la pena nombrar un caso más, porque es el que te devuelve a la humildad correcta. A veces la compensación tampoco se puede ejecutar, o solo deshace una parte.

Imagina que check-credit colocó la retención, inventory-sync falló, y cuando issue-refund intenta liberar la retención, la API de crédito está caída —no por un segundo, sino por horas—. Los reintentos se agotan. La compensación misma quedó pendiente. Ahora tienes una retención huérfana que sabes que hay que liberar pero no puedes liberar en este momento.

Este no es un fallo de tu diseño; es el límite real de lo que la automatización puede resolver sola. Y la respuesta correcta no es inventar una compensación de la compensación hasta el infinito. La respuesta es preservar la intención y escalar a un humano: la intención de liberar sigue en el outbox, marcada como pendiente, con todo su contexto; el consumidor la reintentará cuando la API vuelva; y si tarda demasiado, una alerta le avisa a una persona que hay una retención atascada que quizás haya que liberar a mano desde el panel de crédito.

Fíjate en cómo esto encadena con las dos lecciones que siguen. La intención que no se pierde es la cola de mensajes muertos de la lección 5. El aviso a la persona cuando algo lleva demasiado tiempo atascado es la alerta de la lección 4. Una compensación que falla no es el fin del mundo: es exactamente el caso que las siguientes dos piezas del módulo están diseñadas para atrapar. El sistema no promete que todo se resuelva solo; promete que nada se pierde en silencio.

Errores comunes

Ejecutar la compensación en caliente, en el mismo momento del fallo (conceptual). Qué pasa: cuando inventory-sync falla, se llama directamente al reembolso ahí mismo, en la misma ejecución, en el catch del error. Funciona en las pruebas. En producción, un día el fallo de inventario coincide con un problema en la API de crédito, la compensación en caliente también falla, y ahora tienes una retención huérfana y una compensación perdida, sin registro de que había que compensar. Por qué pasa: llamar al reembolso en el momento se siente lo más natural y directo. Cómo detectarlo: si tu compensación vive dentro del mismo nodo o rama que detectó el fallo, sin pasar por una tabla intermedia, está en caliente. Cómo corregirlo: separa decidir de ejecutar, con el patrón outbox del Módulo 5. Escribes la intención de compensar de forma confiable —eso casi no falla, es una escritura local—, y un consumidor la ejecuta después con sus propios reintentos. Si el sistema se cae entre las dos, la intención sigue ahí.

Olvidar que la compensación se puede duplicar (conceptual). Qué pasa: se diseña la liberación de la retención con todo cuidado, pero sin clave de idempotencia, porque "es una compensación, es lo que arregla las cosas, ¿qué podría salir mal?". Un reintento del consumidor libera dos veces, y según la API eso devuelve crédito de más o rompe el ledger. Por qué pasa: mentalmente, la compensación se siente como "la buena", la que limpia, y uno baja la guardia. Cómo detectarlo: revisa cada acción compensatoria y pregúntate lo mismo que a cualquier efecto: "¿si esto corre dos veces, hace daño?". Cómo corregirlo: asígnale a la compensación su propia Idempotency-Key y marca en el ledger cuándo ya se compensó, para no volver a intentarlo. La compensación juega con las mismas reglas que la operación original.

Diseñar para un rollback perfecto que no existe (conceptual). Qué pasa: se asume que "deshacer" devuelve el sistema a un estado idéntico a "nunca pasó", y se manda el correo de confirmación temprano confiando en que "si algo falla, lo compensamos". Cuando algo falla, resulta que el correo ya se leyó y no hay compensación que lo retire; el cliente ya se enteró de un pedido que después se canceló. Por qué pasa: la palabra "rollback" arrastra la intuición de las bases de datos, donde revertir sí es perfecto. Cómo detectarlo: para cada efecto de tu sistema, clasifícalo como "reversible" (retención, reserva) o "irreversible" (correo, SMS, cobro con comisión). Si tienes efectos irreversibles ocurriendo antes que efectos que pueden fallar, tienes un problema latente. Cómo corregirlo: ordena los efectos para que los irreversibles ocurran al final, cuando ya no hay riesgo de tener que deshacerlos, y acepta que el estado tras una compensación es aceptable, no idéntico. Diseña para el estado aceptable.

Ejercicios

Ejercicio 1 — Empareja cada operación con su compensación. Para cada una de estas operaciones de Cumbre, escribe su acción compensatoria si la tiene, o marca "sin compensación limpia" y propón la mejor corrección posible:

(a) Colocar una retención de crédito de 4820 pesos. (b) Reservar 20 kilos de café arábica en el inventario. (c) Enviar un WhatsApp de "tu pedido va en camino". (d) Cobrar 4820 pesos a la tarjeta del cliente.

Ver solución

(a) Compensación limpia: liberar la retención. Devuelve la línea de crédito al estado anterior con muy poco rastro. Es de las más limpias del sistema.

(b) Compensación limpia: devolver el stock reservado. El inventario vuelve a estar disponible. También muy limpia, siempre que la reserva y su devolución sean idempotentes.

(c) Sin compensación limpia. El mensaje ya llegó y quizás ya se leyó; no se puede retirar. La mejor corrección posible es otra comunicación: un segundo WhatsApp que diga "hubo un problema con tu pedido, lo estamos resolviendo". No deshace el primero; lo corrige socialmente. La lección de diseño: este mensaje no debería haberse mandado hasta estar seguros de que el pedido es viable —debe ir al final del orden de efectos—.

(d) Compensación: emitir un reembolso, pero con rastro y costo. El reembolso devuelve el dinero, pero deja dos movimientos en el estado de cuenta del cliente y, según la pasarela, puede costar una comisión que no se recupera. Es aceptable, no idéntico. Por eso el cobro definitivo, como el correo, conviene dejarlo para el final.

Por qué funciona: el ejercicio te obliga a clasificar cada efecto por qué tan limpia es su compensación, que es la información que después decide el orden en que los ejecutas. Los efectos con compensación limpia (a, b) pueden ir primero; los que dejan rastro o son irreversibles (c, d) van al final.

Ejercicio 2 — Ordena los efectos. Cumbre procesa un pedido con estos cuatro efectos: (1) colocar la retención de crédito, (2) reservar el inventario, (3) mandar el WhatsApp de confirmación al cliente, (4) hacer el cobro definitivo a la tarjeta. Cualquiera de los cuatro puede fallar. Propón un orden de ejecución que minimice la necesidad de compensaciones irreversibles, y justifícalo.

Ver solución

Un orden razonable: retención → reserva de inventario → cobro definitivo → WhatsApp.

El razonamiento, paso a paso:

  • La retención va primero porque es reversible y barata de deshacer, y porque verifica lo más probable de fallar (crédito insuficiente) antes de tocar el inventario. Si falla, no se hizo nada más.
  • La reserva de inventario va segunda, también reversible. Si falla, solo hay que liberar la retención —una compensación limpia—.
  • El cobro definitivo va tercero, cuando ya sabes que hay crédito y hay stock. Es más caro de deshacer (deja rastro, cuesta comisión), así que solo lo haces cuando el pedido es casi seguro. Si falla el cobro, liberas la retención y la reserva —ambas limpias—.
  • El WhatsApp va al final, cuando todo lo demás ya tuvo éxito. Es irreversible, así que solo debe salir cuando no hay ningún riesgo de tener que desmentirlo.

La idea central: cada efecto está ordenado de "más reversible" a "menos reversible". Así, en cualquier punto donde algo falle, todo lo que hay que compensar hacia atrás es reversible, y lo irreversible nunca llegó a ocurrir. Un orden distinto —mandar el WhatsApp primero, por ejemplo— te obliga a desmentir mensajes cada vez que algo se cae más adelante.

Por qué funciona: el orden de los efectos es una de las decisiones de diseño más baratas y más poderosas de un sistema resiliente. No cuesta nada elegirlo bien desde el principio, y evita una categoría entera de compensaciones imposibles. Es exactamente el tipo de decisión que se defiende en una entrevista mostrando tu grafo.

Ejercicio 3 — La compensación duplicada. El consumidor del outbox de Cumbre que ejecuta release_credit_hold tiene Retry On Fail activado. Un día, la API de liberación responde lento: libera la retención pero la confirmación se pierde, y n8n reintenta. Describe qué pasa (a) si la liberación NO es idempotente, y (b) si lleva su clave de idempotencia y el estado en el ledger. ¿Qué dos protecciones evitan el doble deshacer?

Ver solución

(a) Sin idempotencia: es el mundo B de la lección 2 aplicado a una compensación. La primera petición liberó la retención, pero como la confirmación se perdió, n8n ve un timeout y reintenta. La segunda petición pide liberar otra vez la misma retención. Según cómo esté hecha la API, esto puede: devolver un error confuso ("esta retención ya no existe"), o —peor— devolver crédito de más si la API interpreta cada liberación como un abono independiente. En cualquier caso, el ledger puede quedar inconsistente. Deshacer dos veces es un bug tan real como hacer dos veces.

(b) Con las dos protecciones: el reintento manda la misma Idempotency-Key derivada del credit_hold_id, así que la API reconoce que ya procesó esa liberación y devuelve el resultado existente sin liberar de nuevo. Y aunque la clave fallara, el sistema consulta el ledger antes de intentar: si hold_status ya es 'released', ni siquiera manda la petición. Resultado: la retención se libera exactamente una vez.

Las dos protecciones son: (1) la clave de idempotencia en la petición, que protege del lado de la API externa; y (2) el estado en el ledger (hold_status), que protege del lado del sistema evitando que se intente compensar algo ya compensado. Las dos juntas cubren tanto el reintento del nodo como una segunda intención que llegara por cualquier razón.

Por qué funciona: este ejercicio cierra el círculo del módulo. La compensación no es una zona libre de las reglas de idempotencia; es un efecto más que necesita las mismas dos protecciones que cualquier operación. Reembolsar dos veces, liberar dos veces, devolver stock dos veces: todos son duplicados, y todos se previenen igual.

Resumen y siguiente paso

En esta lección viste que cuando un efecto ya ocurrió y no puedes hacerlo idempotente ni evitarlo, la salida es deshacerlo con una acción compensatoria: por cada operación que crea algo, defines de antemano la que lo anula, como el reembolso del vuelo cuando el hotel no tiene lugar. Entendiste por qué existe el patrón —no hay una transacción atómica que abarque varios sistemas distintos, y una secuencia de pasos con sus compensaciones es lo que la ingeniería llama una saga— y viste el flujo completo de Cumbre: check-credit coloca una retención, inventory-sync falla, y issue-refund la libera, con la decisión de compensar separada de la ejecución vía el outbox del Módulo 5 y el estado rastreado en el ledger del Módulo 4. Aprendiste que el rollback perfecto casi nunca existe —hay efectos irreversibles, la compensación deja rastro, y hay una ventana de inconsistencia— y que por eso diseñas para un estado aceptable, no idéntico, ordenando los efectos para que lo irreversible ocurra al final. Y viste que la compensación también tiene que ser idempotente, protegida por su propia clave y por el estado en el ledger.

Antes de avanzar deberías poder: dar la compensación de una operación cualquiera de tu sistema, o reconocer que no tiene una limpia; explicar por qué un correo enviado no se puede compensar y qué haces en su lugar; y ordenar una lista de efectos de más reversible a menos reversible con su justificación.

Lo que no viste todavía es la decisión humana. Un reintento se cura solo; una compensación limpia el estado sola. Pero hay fallos que ningún mecanismo automático puede resolver —una compensación que también falla, un reembolso atascado, un pedido que quedó en un limbo— y que necesitan que un humano se entere y actúe. La lección 4 es sobre esa decisión: cuáles fallos merecen una alerta que despierta a alguien y cuáles solo un log, cómo definir qué es un fallo "real" para cada workflow, y por qué alertar de todo es la forma más rápida de que nadie mire ninguna alerta.

Recursos

  • Error handling — n8n Docs — el manejo de errores del nodo que dispara la detección de que hay que compensar, incluyendo la salida de error que enruta el fallo por su propio camino.
  • Handle errors gracefully — n8n Docs — guía oficial de diseño de manejo de errores, base para decidir dónde detecta el sistema que un paso quedó a medias.
  • HTTP Request node — n8n Docs — el nodo con el que issue-refund llama a la API de liberación y de reembolso, donde vive la clave de idempotencia de la compensación.
  • Postgres node — n8n Docs — el nodo con el que se escribe la intención en el outbox y se actualiza el estado del efecto en el ledger, sobre el Postgres del Starter Kit.