Episodio 4

Cuando algo falla a mitad

lectura · 15 mindominio · BCPS Bankoperación · transferencia a otro banco

Una deuda pendiente

En el episodio 1, Ana recibió un error, pulsó «Reintentar» y Luis cobró dos veces. Lo dejamos sin resolver. Hoy toca pagarlo, y no viene solo.

La transferencia a otro banco del episodio 3 tiene varios pasos, y entre paso y paso pueden pasar cosas. Una red que pierde una respuesta. Un servicio que se reinicia en mal momento. Un broker que, por asegurarse, entrega el mismo evento dos veces. Ninguna es rara: en un sistema con suficiente tráfico, pasan todos los días.

Lo peligroso no es que algo falle, sino que falle a mitad: cuando unos pasos ya se han hecho y otros no. Este episodio presenta tres piezas que, juntas, hacen que una operación termine bien o termine deshecha, pero nunca a medias ni repetida: la saga, el outbox y la idempotencia.

Cinco formas de fallar

A la izquierda, la transferencia sin protecciones: cargar, enviar, publicar. A la derecha, con las tres piezas. Marca uno o varios fallos y pulsa Enviar. Cada envío empieza desde cero, con 1.000 € en la cuenta de Ana, y al terminar el recuadro Resultado marca en verde lo que ha quedado como debía y en rojo lo que no.

Qué ha pasado

Saga: reservar primero, compensar si hace falta

Sin protecciones, Transferencias carga los 100 € a Ana y luego los envía. Si el banco de Luis rechaza, el dinero ya ha salido y el código no tiene ningún camino para devolverlo.probar en el laboratorio No se puede hacer un rollback: el cargo se hizo en otra transacción, en otro servicio, hace un rato.

Una saga es una operación larga partida en pasos locales, cada uno con su propia transacción, donde cada paso tiene una compensación: otro paso que deshace su efecto. Si algo falla más adelante, se ejecutan las compensaciones de lo que ya se hizo, en orden inverso.

PasoCompensación
Retener 100 € en la cuenta de AnaLiberar la retención
Enviar al banco de Luis— (si lo acepta, ya no hay vuelta atrás)
Convertir la retención en cargo— (solo se hace si el banco aceptó)

Fíjate en dos detalles. Primero, el orden: se retiene antes de enviar y se carga después de saber la respuesta. La retención es una reserva: el dinero sigue en la cuenta de Ana pero no puede gastarlo. Si hay que compensar, liberar una retención es limpio; devolver un cargo deja dos movimientos en el extracto y un rato en el que Ana ve menos dinero del que tiene.

Segundo, compensar no es borrar. Es una acción nueva, que deja rastro. Igual que en el libro de movimientos de Cuentas un error no se borra sino que se corrige con otro movimiento. Y hay pasos que no se pueden compensar: una vez que el banco de Luis ha abonado el dinero, ya no se puede «desenviar». Por eso los pasos irreversibles van lo más tarde posible.

Outbox: el evento no puede quedarse en memoria

Ahora tira Transferencias justo después de guardar el resultado.probar en el laboratorio Sin protecciones, la base de datos dice «COMPLETADA», pero TransferenciaCompletada nunca se publica: Ana no recibe el aviso y Reporting no anota nada. No hay ningún error en ningún sitio; simplemente falta algo.

Es el problema de la doble escritura: hay que escribir en dos sitios (la base de datos y el broker) y no hay una transacción que abarque los dos. Cambiar el orden no ayuda. Si publicas antes de guardar y te caes entre medias, has anunciado una transferencia que tu base de datos no conoce.

El outbox lo resuelve convirtiendo dos escrituras en una. Transferencias guarda el resultado y el evento en su propia base de datos, en una tabla outbox, en la misma transacción: o se guardan los dos o ninguno. Después, un proceso aparte (el relay) lee el outbox, publica en el broker y, cuando el broker confirma, marca la fila como enviada. Si Transferencias se cae, el evento sigue en la tabla y el relay lo publica al volver.

El relay también se puede caer: justo después de publicar y antes de marcar la fila. Al volver, la publicará otra vez. El outbox garantiza que el evento sale al menos una vez, no exactamente una. Por eso hace falta la tercera pieza.

Idempotencia: que repetir no cambie nada

Una operación es idempotente si hacerla dos veces tiene el mismo efecto que hacerla una. «Poner el saldo a 900 €» lo es; «restar 100 €» no. En un sistema distribuido todo se acaba repitiendo, así que la pregunta no es cómo evitar las repeticiones, sino cómo hacer que no importen. En la simulación hay tres sitios donde se repite algo:

Las tres son la misma idea: un identificador estable por intento y alguien que recuerda cuáles ha visto. Lo que cambia es quién pone el id y quién lo recuerda.

Las tres juntas

Marca «Todo a la vez».probar en el laboratorio Sin protecciones, Luis recibe 300 €, Ana pierde 200 y recibe un aviso de más. Con las tres piezas, el resultado es el mismo que si nada hubiera fallado. Se necesitan entre sí: la saga sin outbox pierde pasos, el outbox sin idempotencia duplica eventos, y la idempotencia sin saga no deshace nada.

Lo que cuestan

Criterio

No hace falta todo en todas partes. Busca en tu operación qué puede fallar y pon la pieza que toca:

Si en tu operación…PonEn BCPS Bank
Hay varios pasos en servicios distintos y alguno puede fallar después de otroSaga, con una compensación por paso y los pasos irreversibles al finalRetener → enviar → cargar o liberar
Guardas algo y tienes que publicar un evento sobre elloOutboxTransferencias guarda el resultado y TransferenciaCompletada juntos
Un cliente puede reintentar una petición que cambia algoClave de idempotencia en la peticiónLa app de Ana manda K1 en cada intento
Reintentas llamadas a otro sistema tras un timeoutUn id estable por operación, y que el otro lo respeteEl envío T-1 al banco de Luis
Consumes eventos (de cualquier broker)Consumidor idempotente: recordar lo procesadoNotificaciones y Reporting

Y dos reglas que salen de todo lo anterior:

En el mundo real

La palabra saga viene de un artículo de Hector Garcia-Molina y Kenneth Salem de 1987, pensado para transacciones largas dentro de una misma base de datos. Chris Richardson la popularizó para microservicios; en su web (microservices.io) están descritos también el outbox y el consumidor idempotente.

Las claves de idempotencia se hicieron conocidas con la API de Stripe, que acepta una cabecera Idempotency-Key en cada petición que cambia algo y guarda la respuesta 24 horas. Hay un borrador del IETF para estandarizar esa cabecera, y muchas APIs de pagos la siguen.

Para el relay del outbox hay dos formas habituales: consultar la tabla cada poco tiempo, o leer el registro de cambios de la base de datos (change data capture). Debezium hace lo segundo y trae un componente específico para outbox. Kafka tiene productores idempotentes y transacciones que dan «exactamente una vez» dentro de Kafka; en cuanto el efecto sale de Kafka (un aviso, un abono en otro banco), vuelves a necesitar idempotencia en el consumidor.

Y los sistemas de pagos entre bancos llevan un identificador de extremo a extremo en cada operación precisamente para lo que hace T-1 en la simulación: que un reintento no se convierta en dos pagos.

Siguiente episodio

En toda la serie, el broker ha sido una caja genérica que guarda mensajes hasta que alguien los recoge. Pero no todos los brokers hacen lo mismo con un mensaje una vez entregado: unos lo borran y otros lo guardan. La diferencia parece un detalle de implementación hasta que Reporting descubre un error en el extracto de marzo.

Siguiente · Episodio 5 Log vs cola Reporting necesita reprocesar los movimientos de todo el mes. ¿Siguen en algún sitio? Y qué significa de verdad «al menos una vez».

← Volver al mapa de BCPS Bank