Transactional Outbox паттерн используется для надежной публикации событий, когда нужно одновременно сохранить изменения в БД и отправить сообщение в брокер.
В распределенных системах нельзя объединить в транзацию сохранение данных в БД и отправку события в брокер сообщений, т.к. это 2 разных системы не объединенные общей системой транзакционности. И правило атомарности для двух операций вместе не действует.
Для решения этой проблемы нам приходит на помощь Transactional Outbox паттерн.
Проблема
Вам нужно сохранить заказ в БД и отправить сообщение об этом в RabbitMQ или Kafka.
Объединить это в транзацию как мы уже обсуждали - не получится.
Значит нужно сделать 2 отдельные атомарные операции.

Но и тут нас ждет проблема.
А что если данные сохранятся в БД, но потом произойдет сбой микросервиса, брокера сообщений или оборвется соединение?

В этом случае данные в нашей распреденной системе будут неконсистентны. Т.к. повторить отправку мы уже не сможем.
Здесь нам и пригодится паттерн Transactional Outbox.
Решение
Для решения этой проблемы воспользуемся транзакционными возможностями реляционной БД. Реляционная БД поддерживает транзакции и соответствует ACID требованиям.
Самое важное требование которое нам необходимо - это (А) Атомарность.
Атомарность гарантирует что операции в транзакции выполнятся все или транзакция не будет применена вообще и произойдет rollback.
Как раз то что мы не могли сделать с двумя системами: БД и брокером сообщений.
Для реализации этого в реляционной БД нам нужно будет создать outbox таблицу.
Эта таблица необхоима для записи events которые необхоимо отправить в брокер сообщений.
Рассмотрим как это выглядит на диаграммах


На стороне Order Service приложения оборачиваем все в одну транзакцию: вставку в orders и создание event в outbox таблице.
BEGIN TRANSACTION;
INSERT INTO orders (id, customer_id, total) VALUES ('order-123', 456, 1000);
INSERT INTO outbox_events (id, aggregate_type, aggregate_id, event_type, payload)
VALUES (uuid_generate_v4(), 'Order', 'order-123', 'OrderCreated',
'{"orderId": "order-123", "total": 1000}');
COMMIT;Эта транзакция выполнится атомарно либо все, либо ничего и тогда клиент получит ошибку и будет вынужден повторить запрос.
Далее Publisher (отд. worker) берет сообщения из outbox и отправляет их в Брокер сообещний.
Publisher это отдельный сервис работающий в беграунде.
Может быть:
- cron job
- polling worker
- CDC (Change Data Capture)
Только после подтверждения что сообщение получено и сохранено, publisher идет в outbox таблицу и удаляет event.
Подводные камни
Сообщение в Message Broker может быть отправлено дважды.
Например из-за проблем:
с сетью (брокер сообщений получит мессадж отправит ack но сообщение не дойдет)
или если worker упадет в момент получения ack, но запись из
outboxне успеет удалить
Для этого используйте идемпотентный ключ и обрабатывайте event на стороне Consumer - подробнее Idempotent Consumer
Где применять
Любая Event Driven architecture, где необходима гарантия доставки сообщений.
Ниже представил пример использования Outbox и CQRS вместе, для построения проекции.
