Referencia

Arquitectura de Software

¿Qué es el patrón Outbox?

Una explicación canónica del patrón outbox: cómo actualizar una base de datos y publicar un evento sin que ambos se separen en silencio cuando uno de los dos falla.

El código parece inocente. Guarda el pedido en la base de datos y luego publica un evento pedido.creado para que el resto del sistema pueda reaccionar. Dos líneas, una tras otra. En una demo funciona siempre.

En producción, tarde o temprano, no. Entre esas dos líneas el proceso puede caerse, el broker puede quedar inalcanzable, la red puede parpadear. Y cuando ocurre, te queda una de dos mentiras silenciosas: un pedido que existe pero que nunca se anunció, o un anuncio de un pedido que nunca se guardó. Nadie lanzó un error. Los dos almacenes simplemente discrepan en silencio, y los sistemas posteriores actúan sobre la versión equivocada de la realidad.

El patrón outbox existe para que esas dos líneas se comporten como una sola.

El problema: la doble escritura

Este es el problema de la doble escritura, y no tiene solución limpia mientras trates la base de datos y el broker como dos cosas que actualizar en secuencia. Envolver ambas en un try/catch no ayuda: reintentar la publicación tras una caída requiere saber que falló, y ese conocimiento murió con el proceso. Las transacciones distribuidas entre una base de datos y un broker de mensajes (commit en dos fases) pueden, técnicamente, ligarlos, pero son lentas, están mal soportadas por los brokers modernos y son una conocida trampa operativa.

El arreglo honesto es dejar de hacer dos escrituras. Haz una.

Cómo funciona

Definición

Patrón Outbox

Una técnica que hace que un cambio de estado y su evento saliente sean atómicos al escribir el evento en una tabla outbox dentro de la misma transacción de base de datos que el cambio de estado. Un proceso aparte lee luego las filas no publicadas de esa tabla y las entrega al broker, marcando cada una como enviada.

La clave es que tu base de datos ya te da atomicidad, dentro de una sola transacción. Así que, en lugar de publicar al broker directamente, escribes el evento como una fila en una tabla outbox, en la misma transacción que escribe el pedido:

BEGIN;
INSERT INTO pedidos (id, cliente_id, total) VALUES (...);
INSERT INTO outbox (id, topic, payload, publicado_en)
VALUES (gen_random_uuid(), 'pedido.creado', '{...}', NULL);
COMMIT;

Ahora hay exactamente una escritura, y o bien se confirma por completo o se revierte por completo. El pedido y su evento pendiente se guardan juntos o no se guardan: la doble escritura desaparece.

Un segundo componente, el relay (o despachador), hace la publicación real. Sondea la outbox por filas donde publicado_en IS NULL, envía cada una al broker y la marca como publicada:

Bucle del relay:
filas = SELECT * FROM outbox WHERE publicado_en IS NULL ORDER BY id
para cada fila:
broker.publicar(fila.topic, fila.payload) // puede reintentar
UPDATE outbox SET publicado_en = now() WHERE id = fila.id

Si el relay se cae a mitad del bucle, las filas sin marcar siguen ahí al reiniciar; simplemente retoma donde se quedó. Una variante más pulida lee el registro de replicación de la base de datos en lugar de sondear —captura de cambios (change data capture), como hace Debezium—, pero la garantía es la misma: el evento es duradero en el instante en que la transacción confirma, y la entrega es un asunto aparte y reintentable.

Este es el mismo linaje que el registro de eventos en el corazón de event sourcing y de la mensajería que impulsa la arquitectura orientada a eventos: la outbox es el puente que permite a un servicio convertir de forma confiable un commit local en un mensaje en el que otros pueden confiar.

Compensaciones

La outbox compra confiabilidad, y la cobra.

  • Al-menos-una-vez, no exactamente-una-vez. El relay puede caerse después de publicar pero antes de marcar la fila como enviada, así que el mismo evento puede entregarse dos veces. Los consumidores deben ser idempotentes: apoya las reacciones en el ID del evento para que una reproducción sea inofensiva. Esto no es un defecto que arreglar; es el contrato.
  • Latencia. Un relay que sondea añade un pequeño retraso entre el commit y la publicación. Suelen ser milisegundos, pero no es instantáneo.
  • Superficie operativa. Ahora operas y monitoreas un relay, y la tabla outbox necesita poda para que no crezca sin fin.
  • El orden requiere cuidado. Si los consumidores dependen del orden de los eventos, el relay debe preservarlo, normalmente publicando por agregado en orden de ID.

Cuándo no usarlo

Recurre a la outbox precisamente cuando un evento perdido o fantasma corrompería el estado posterior. Esa es la situación para la que se diseñó, y fuera de ella la tabla y el relay extra son costo sin beneficio.

Ideas clave

  • La outbox resuelve el problema de la doble escritura: un commit de base de datos y una publicación al broker no pueden compartir una transacción, así que una caída entre ambos deja a los dos en desacuerdo.
  • Funciona escribiendo el evento en una tabla outbox dentro de la misma transacción que el cambio de estado, haciéndolos atómicos, y luego entregando las filas al broker por separado.
  • La entrega es al-menos-una-vez, así que los consumidores deben ser idempotentes; esto es el contrato del patrón, no un defecto.
  • Se combina de forma natural con event sourcing y la arquitectura orientada a eventos: es como un servicio convierte un commit local en un mensaje en el que otros pueden confiar.
  • Úsalo solo donde un evento perdido sea inaceptable; para señales de mejor esfuerzo, publicar directamente está bien.

La outbox no hace imposible perder la publicación de un evento. Hace que la pérdida sea recuperable, porque el evento está a salvo en disco, dentro del mismo commit que la verdad que describe, esperando a ser enviado de nuevo.

// seguir explorando

Seguir explorando