Покупатель оформил заказ, получил подтверждение, а склад так и не увидел задачу на сборку. Сервис заказов исправен, его база сохранила данные. Сообщение о заказе не дошло до остальных систем. Такой случай неприятен именно тем, что каждая сторона может показывать зелёные индикаторы.
Причина часто скрыта в двух отдельных действиях: сохранить заказ и опубликовать событие. Между ними есть момент, когда приложение может упасть. Transactional Outbox нужен, чтобы этот небольшой технический промежуток не превращался в потерянное обязательство перед клиентом.
Почему два успешных вызова — ещё не одна операция
Допустим, сервис сначала записывает заказ в базу, а потом отправляет сообщение брокеру. Если запись прошла, но приложение завершилось до публикации, в базе заказ есть, а у склада — нет. Поменяем порядок? Если сначала отправить сообщение, а потом база отклонит транзакцию, склад получит событие о заказе, которого никогда не существовало.
Проверить «обе операции по очереди» недостаточно. Нужна согласованность: либо сохраняются и бизнес-изменение, и намерение отправить сообщение, либо не сохраняется ни то ни другое.
Как Outbox закрывает разрыв
Внутри одной локальной транзакции приложение записывает заказ и отдельную запись в outbox — таблицу или другое хранилище исходящих сообщений в той же транзакционной границе. Затем транзакция подтверждается.
Другой процесс забирает ожидающие записи и публикует их в брокер. Он может опрашивать таблицу или читать журнал изменений базы. Если брокер временно недоступен, запись остаётся и отправка возобновляется позже.
Outbox не делает базу и брокер одним хранилищем. Он гарантирует, что намерение отправить сообщение сохраняется вместе с бизнес-фактом. Для этого и нужна общая транзакция именно на этапе записи, а не вокруг всей распределённой системы.
Почему событие теперь может прийти дважды
Публикующий процесс отправил сообщение, брокер его принял, а подтверждение не успело сохраниться в outbox. После перезапуска процесс отправит запись ещё раз. Это нормальная цена восстановления после неопределённого исхода.
Поэтому получатель должен распознавать повторное событие. Например, если обработка события начисляет бонус, ей нужен устойчивый идентификатор операции и способ не начислить его второй раз. Идемпотентность — обязанность потребителя, а не волшебное свойство outbox.
Отдельного внимания требует порядок сообщений. Когда один заказ последовательно меняет статус, параллельные издатели не должны произвольно менять причинную последовательность без предусмотренной защиты. В зависимости от системы это решают ключами, последовательными номерами и правилами доставки.
Кто заметит очередь, которая перестала двигаться
Команда может правильно реализовать outbox и всё равно потерять управляемость. Например, публикация прекратилась, таблица растёт, а никто не знает, сколько заказов ждут передачи. Поэтому нужно измерять возраст самой старой необработанной записи, число неудачных попыток, отставание публикации и состояние получателей.
Нужны правила очистки успешно отправленных записей, повторной обработки проблемных событий и безопасного восстановления после длительной остановки. Если сообщения связаны с деньгами или исполнением заказов, у процесса должен быть владелец, способный объяснить бизнесу, что сейчас не завершено.
Когда можно обойтись без него
Если никакой второй системы нет и изменение остаётся внутри одной базы, не нужно создавать брокер ради применения паттерна. Если сервис использует платформу с уже обеспеченной атомарной публикацией, сначала разберитесь в конкретных гарантиях платформы.
Outbox стоит внедрять там, где между записью бизнес-факта и отправкой сообщения есть опасное окно отказа, а потеря уведомления приводит к реальной ошибке процесса.
Outbox защищает от потерянного сообщения, но не освобождает от повторов и операционного контроля. «Отправлено надёжно» — это ещё не «обработано ровно один раз».
Технические варианты и ограничения описаны в паттерне Transactional Outbox и AWS Prescriptive Guidance.