¿Qué es el patrón transactional outbox, y cómo resuelve el problema de la doble escritura entre una base de datos y un message broker?
Escribir en tu base de datos y publicar en un broker son dos sistemas separados sin transacción compartida -- el patrón outbox convierte eso en una única escritura atómica más un relay confiable, de modo que jamás puede fallar justo entre las dos.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué es el patrón transactional outbox, y cómo resuelve el problema de la doble escritura entre una base de datos y un message broker?
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.#El problema de la doble escritura
Supongamos que un servicio de órdenes necesita hacer dos cosas cuando se coloca un pedido: persistir la orden en su base de datos, y publicar un evento OrderCreated para que inventario, facturación y notificaciones puedan reaccionar. Son dos sistemas independientes sin transacción compartida entre ellos -- una base de datos relacional no participa de la misma transacción ACID que un broker de Kafka o un exchange de RabbitMQ. Si confirmás primero la escritura en base de datos y después publicás, una caída o una interrupción del broker entre esos dos pasos deja una orden persistida de la que nadie downstream se entera jamás. Si publicás primero y confirmás después, un commit fallido deja un evento que describe una orden que no existe. Envolver ambas cosas en una transacción de dos fases (2PC) es posible en teoría pero rara vez está disponible o es deseable en la práctica -- la mayoría de los brokers no soportan XA, y los propios modos de falla de 2PC (un coordinador que muere a mitad del commit) son otro problema en sí mismos.
#La solución: hacer atómica solo una escritura, y retransmitir el resto
El patrón transactional outbox esquiva el problema en vez de resolverlo de frente: en lugar de escribir en dos sistemas, escribís en uno solo. La fila de negocio y un registro del evento a publicar entran juntos en la misma transacción local de base de datos, en una tabla outbox. Como eso es una única transacción ACID contra una única base de datos, es trivialmente atómica -- o existen ambas filas, o no existe ninguna, sin ninguna ventana intermedia. Un proceso separado, el relay (o poller), lee las filas no publicadas del outbox y las publica en el broker con su propio calendario, independiente de la petición original.
#El lado de la escritura: fila de negocio más fila de outbox, una transacción
import jsonimport uuiddef create_order(conn, order):cur = conn.cursor()cur.execute('INSERT INTO orders (id, customer_id, total_cents, status) ''VALUES (%s, %s, %s, %s)',(order['id'], order['customer_id'], order['total_cents'], 'CREATED'))payload = json.dumps({'order_id': order['id'],'customer_id': order['customer_id'],'total_cents': order['total_cents'],})cur.execute('INSERT INTO outbox (id, aggregate_type, aggregate_id, event_type, ''payload, created_at, published) ''VALUES (%s, %s, %s, %s, %s, now(), false)',(str(uuid.uuid4()), 'Order', order['id'], 'OrderCreated', payload))conn.commit() # una sola transaccion ACID: existen ambas filas, o ningunacur.close()
#El relay: publicar, y después marcar como publicado
import timedef relay_loop(conn, producer, poll_interval_seconds=1):while True:cur = conn.cursor()cur.execute('SELECT id, aggregate_id, event_type, payload FROM outbox ''WHERE published = false ORDER BY created_at ASC LIMIT 100')rows = cur.fetchall()for row_id, aggregate_id, event_type, payload in rows:producer.produce(topic='order-events',key=aggregate_id,value=payload,headers={'event_type': event_type})producer.flush()# Se marca como publicado solo despues de que el broker tiene el mensaje.# Un fallo entre produce() y este UPDATE causa una publicacion# duplicada, no una perdida -- los consumidores deben deduplicar.cur.execute('UPDATE outbox SET published = true WHERE id = %s',(row_id,))conn.commit()cur.close()time.sleep(poll_interval_seconds)
Fijate el orden de las operaciones dentro del loop: primero publicar, después marcar como publicado. Invertir ese orden reabriría exactamente el problema de doble escritura que el patrón existe para cerrar -- un fallo después de marcar como publicado pero antes de que el broker realmente tenga el mensaje descartaría el evento en silencio. Publicar primero y marcar después deja como único modo de falla una publicación duplicada, que es la razón por la cual el patrón outbox y los consumidores idempotentes están pensados para usarse juntos -- ver el patrón de tabla de deduplicación para el lado consumidor de este mismo problema.
#Dos formas de construir el relay
| Aspecto | Relay por polling | Relay basado en CDC |
|---|---|---|
| Cómo lee el outbox | SELECT ... WHERE published = false periódico | Sigue directamente el write-ahead log o binlog de la base de datos |
| Latencia | Acotada por el intervalo de polling, típicamente segundos | Casi en tiempo real, típicamente milisegundos |
| Carga sobre la base de datos | Consultas de polling repetidas, incluso en reposo | Mínima -- lee el stream del log, no la tabla |
| Complejidad operativa | Simple -- un worker tipo cron | Más alta -- necesita un conector CDC como Debezium más Kafka Connect |
| Garantía de entrega | At-least-once | At-least-once |
🧠 Check your understanding
0/3 · 0/3 answered1. ¿Qué fallo específico previene el patrón transactional outbox?
2. ¿Por qué la fila del outbox debe escribirse en exactamente la misma transacción local que la fila de negocio?
3. ¿Qué garantía de entrega le da el relay al message broker, y qué implica eso para los consumidores?