
События разворачивались в нескольких реальностях. В первой заказ не пришёл. Во второй - произошло несколько списаний средств. И заказ пришёл. А может даже не один. В третьей система упала посередине процесса, оставив данные в не консистентном состоянии.
А четвертая оказалось самой интересной. Где даже с неполадками сети и падениями частей сервиса был сделан лишь один заказ. И произведена одна оплата. Благодаря чему пришла желаемая теплая пицца. Которую хотел наш дорогой покупатель.
Погрузимся в проблематику оформления заказа и схлопнем все реальности в нужную с помощью Outbox Pattern.
Outbox pattern за 30 секунд
Базовая архитектура

Раскроем сервис заказа пиццы:
Orders Service — содержит основную бизнес-логику по обработке заказа.
Orders DB — реляционная СУБД. Имеет таблицы: orders, outbox.
Outbox Relay/worker — компонент, который читает таблицу outbox и пишет в брокер, помечая записи как доставленные.
Message Broker — наш middleware с очередями/топиками для передачи событий.
Consumers — Billing/Inventory и др. с идемпотентной обработкой.
Базовая логика применения паттерна:
Сервис ордеров хранит заказы в своей БД. При создании заказа в той же транзакции кладёт событие в таблицу outbox. Отдельный процесс вычитывает outbox и публикует в брокер сообщений. Другие сервисы (биллинг, склад) подписываются и обрабатывают события.
Проблематика

Проблема в том, что order service взаимодействуя с окружением может пойти лишь по одному из двух путей:
Сначала сохранить ордер в БД и затем оповестить другой сервис/создать событие.
Сначала оповестить другой сервис, затем сохранить ордер в БД
И в каждом из сценариев где-то посередине сервис может упасть не сделав очередное действие.

Поэтому мы и расшиваем логику в отдельные сущности.
Семантика доставки в распределённых системах

События
Можно заметить, что мы ввели понятие “события“. При проектирование системы можно выбрать нужную архитектуру в зависимости от требований. К примеру, монолит - когда основная логика выполняется в одном процессе. Или же микросервисная архитектура с синхронным взаимодействием - все друг друга ждут.
Outbox pattern живёт в рамках Event Driven Architecture(EDA) - архитектуры, построенной на основе обмена событий.
Примеры:
Создан заказ
Списаны деньги
Зарезервирован товар
Семантика. Теория
Далее нам важно понимать семантику доставки. Она выражает гарантии, которые мы даём потребителям сообщений. На практике чаще всего обсуждают три варианта:
At-most-once — «не более одного раза». Событие может потеряться, но дубликатов не будет.
At-least-once — «как минимум один раз». Событие не потеряется, но возможны дубликаты.
Exactly-once — «ровно один раз». Достигается комбинацией at-least-once + идемпотентность/дедупликация на стороне обработчика.
Семантика. Практика
Мы не хотим терять сообщения. Конечно же, наши сервисы имеют хорошие показатели доступности. Это известные всем и бесполезные с практической точки зрения на интервью 9ки. И сеть почти никогда не рвется. Но именно доли процента отказов на высоких нагрузках будут портить жизнь нашим доро��им пользователям. Больше нагрузка → больше потерь.
Поэтому как и в других аспектах System Design для обеспечения лучших гарантий мы вводим избыточность. Выбираем at-least-once. Да, могут быть дубли. Но мы с ними справимся благодаря идемпотентной обработке.
Паттерн outbox подробней

1. Клиент вызывает POST /api/v1/orders.
2. В единой транзакции:
а) вставляется запись в orders;
б) вставляется запись в outbox (event_id, event_type, payload, occurred_at, status='NEW').
3. Транзакция коммитится → мы атомарно зафиксировали и состояние, и запись/событие. Cпасибо ACID'у(а конкретно какой букве? :) ). На этом наш order service кланяется и может падать сколько хочет. Главное сделано - зафиксирован ордер и создана запись. Что будет дальше - не его дело.
4. Outbox Relay:
Вычитывает новые записи
Публикует в брокер
Переводит запись в статус “обработано“
Он может упасть после публикации в брокер. Поднимется. И снова вычитает туже вроде бы не обработанную запись. Снова перешлёт. И, наконец, переведёт ей в статус “обработано“. Мы пошли на это сознательно. Допускаем такие дубликаты в брокер.

5. Потребители читают из брокера. Каждый обработчик проверяет, не обрабатывали ли уже событие.
Это даёт at-least-once между outbox и брокером, а вместе с идемпотентностью у потребителей — exactly-once effects на уровне выполнения бизнес логики.
Общая схема


Основная таблица для сервиса заказа:
CREATE TABLE orders (
id uuid PRIMARY KEY,
status text NOT NULL,
amount bigint NOT NULL,
created_at timestamptz NOT NULL default now()
);
CREATE TABLE outbox (
event_id uuid PRIMARY KEY,
event_type text NOT NULL,
payload jsonb NOT NULL,
status text NOT NULL CHECK (status IN ('NEW','SENT','ERROR')),
occurred_at timestamptz NOT NULL default now(),
sent_at timestamptz
);Идемпотентность - что это и зачем бизнесу
Идемпотентность
Это свойство операции давать один и тот же наблюдаемый результат при повторном выполнении.
В контексте событий и очередей это означает: если одно и то же событие обработается два, три и больше раз, бизнес‑состояние не поменяется сверх первого применения.
Зачем бизнесу
Защита от двойного списания и двойных доставок, отсутствие «накрутки» бонусов/скидок, предсказуемость SLA при сбоях и ретраях, меньше тикетов в саппорт и выше доверие клиентов. Благодаря идемпотентности можно использовать семантику доставки at‑least‑once и всё равно получать ровно один бизнес‑эффект.
Идемпотентность потребителей: короткий рецепт
Таблица дедупликации в своей БД (а не в БД ордеров):
CREATE TABLE processed_events (
event_id uuid NOT NULL,
handler text NOT NULL,
processed_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (event_id, handler)
);Что важно:
event_id генерирует продюсер при записи в outbox и кладёт в сообщение.
handler чаще всего = billing, inventory-metrics, … (имя consumer group при использование кафки).
Ack/commit offset — только после коммита БД потребителя.
Выводы

Паттерн outbox позволяет развязать логику сохранения заказа и его обработки.
Между outbox и брокером - at-least-once. Поэтому дубликаты неизбежны.
Exactly-once достигается идемпотентностью у потребителя.
На System Design Интервью вы дойдёте до использования паттерна после описание основной схемы. В финальных минутах, на которых зайдёт речь про выявление bottlenecks и повышение отказоустойчивости системы.

Приветствую! Меня зовут Невзоров Владимир. Подробней о нашей исследовательнице сервиса заказа на моём канале System Design World, посвященному Архитектуре, System Design, Highload бэкэнду.