Transactional Outbox / Inbox¶
Раздел 01 · Уровни и общие гарантии
Тип материала: архитектурный разбор. Схемы и примеры иллюстративные, если не указано иное.
Уровень: границы транзакций и надёжная доставка интеграционных событий. Проверка источников: 2026-11-02.
1. Назначение и решаемая проблема¶
Outbox решает проблему «business row commit выполнен, но событие не опубликовано». В одной локальной транзакции записываются business change и durable intent отправки. Relay доставляет intent асинхронно.
Inbox решает другую половину: повторная доставка не должна повторно менять consumer state. Вместе они устраняют опасный dual-write gap, но не образуют одну распределённую транзакцию через все сервисы.
2. Принцип работы¶
Producer сначала формирует стабильный event_id и aggregate version; transaction commit фиксирует business state и outbox row. Relay использует polling или CDC и подтверждённую broker publication.
Consumer записывает уникальную inbox identity и business effect в одной локальной транзакции. Для нескольких подписчиков key обычно (consumer_name, event_id), а не глобальный event_id на все виды обработчиков. Один downstream не должен помешать другому легитимно обработать то же событие.
3. Архитектурная схема¶
flowchart TB
C["Command"] --> T["Одна producer DB transaction"]
T --> B["Business rows"]
T --> O["Outbox rows"]
O --> R["Polling или CDC relay"]
R --> K["Broker"]
K --> I["Consumer DB transaction"]
I --> X["Unique inbox identity"]
I --> E["Business effect в этой БД"]
I --> A["После commit: ack / offset progress"]
4. Walkthrough движения данных¶
- CreatePayment проверяет request idempotency; одна transaction создаёт payment и outbox PaymentCreated.
- Relay читает только committed rows. Polling relay отмечает отправку после подтверждения broker; CDC relay использует log progress и retention contract.
- Broker принял event, relay упал до отметки sent. После restart event публикуется повторно с тем же event_id — это ожидаемое окно duplicate.
- Consumer начинает transaction, пытается вставить inbox key. При конфликте он проверяет, что effect уже был принят, и не применяет его снова.
- При новом key consumer обновляет business tables и commit-ит оба изменения. Только после commit подтверждается delivery.
- Crash до commit приводит к rollback и повторной попытке; crash после commit до ack приводит к dedup без второго effect.
Если effect — внешний HTTP call, локальная inbox transaction не может атомарно включить remote service. Нужно создать downstream outbox/work item с устойчивым idempotency key и reconciliation неизвестного результата.
5. Технологии реализации¶
Реляционная БД с atomic transactions/unique constraints; polling relay с блокировкой/claim protocol либо Debezium CDC; Kafka/RabbitMQ/Pulsar как transport. Outbox row может содержать event_id, aggregate_id, aggregate_version, event_type, schema_version, payload и created_at.
FACT: Debezium 3.3 Outbox Event Router описывает преобразование outbox records в сообщения. Он не делает business transaction атомарной за приложение и не реализует consumer inbox. Документация.
6. Консистентность и гарантии обработки¶
Гарантия producer: если business transaction committed, intent durable в той же БД. Liveness доставки условна: relay работает, broker принимает, retention/cleanup не удалили ещё нужное. Transport часто at-least-once; один DB business effect достигается consumer transaction и unique inbox boundary.
Порядок необходимо проектировать: identity ID/created_at не всегда соответствует commit order; несколько relays способны поменять publication order. Для одного aggregate использовать sequence/version и соответствующий relay/key policy. Claim через SKIP LOCKED сам по себе не доказывает порядок.
Cleanup inbox должен учитывать максимальный replay horizon; удалив dedup identity, система может повторно применить старый event. Повтор с тем же event_id, но другим payload, нельзя молча считать безопасным duplicate: сохранить payload hash или другой проверяемый fingerprint и отправить конфликт владельцу контракта.
7. Отказы и восстановление¶
| Сбой | Recovery | Что проверить |
|---|---|---|
| DB rollback | Ни business change, ни outbox event нет | Relay не читает uncommitted rows |
| Publish successful, sent marker lost | Повтор с тем же event_id | Inbox/idempotency поглощает duplicate |
| Inbox записан отдельно от effect | Изменить transaction boundary | Иначе possible loss либо duplicate |
| Polling relay lease истёк | Другой relay забирает work | Повтор допустим; stale worker не нарушает порядок |
| Connector lag превысил log retention | Re-snapshot/reconciliation | Не объявлять continuity без проверки |
Мониторить oldest pending outbox age, consumer lag и reconciliation backlog. DLQ не считается успешным завершением бизнес-процесса.
8. Масштабирование и узкие места¶
Outbox увеличивает write volume и требует индексов, retention и small transaction design. Polling плохо индексированной таблицы нагружает OLTP. CDC переносит нагрузку на log processing, но добавляет connector operations и риск накопления WAL.
Consumer dedup storage растёт с identities и recovery horizon. Partitioning inbox по времени допустим только при понятной политике поздних replay. Hot aggregate всё ещё требует локальной сериализации; больше relays не ускоряют его без изменения порядка.
9. Преимущества и недостатки¶
Плюсы: явная atomic intent boundary, защита от типового lost event, совместимость с обычным CRUD. Минусы: duplicates остаются, доставка eventual, дополнительные rows/relay и cleanup.
Exactly-once business effect возможен для одной consumer DB при выполнении условий. Этот узкий результат нельзя переименовывать в универсальный exactly-once всех downstream systems.
10. Альтернативные решения¶
Dual Write проще по коду, но имеет split-failure gap без дополнительного protocol. 2PC допустим при реально поддержанных участниках и оценённой доступности. CDC business tables подходит для replication, но не всегда выражает semantic domain event.
Event Sourcing может хранить authoritative events, однако их доставка внешнему broker всё равно требует надёжного протокола.
11. Реальные примеры и границы подтверждения¶
FACT: Shopify в статье 2021 года объясняет transactionally staged job queues через outbox и отдельно указывает возможность повторной постановки job. Shopify Engineering. Формулировка источника описывает способ решения их проблемы, а не все настройки production relay.
FACT: slides BEC/Scoutz (Current London 2025, PDF p.32–34, номера slides 32–34) показывают DB2 outbox, CDC и отдельную reconciliation. Slides. Это дополнительный банковский пример сочетания механизмов; из рисунка не выводится atomic end-to-end contract.
12. Применение в FinTech и iGaming¶
FinTech: payment state и PaymentCaptured intent фиксировать вместе; reporting consumer использует inbox в своей БД. Provider capture защищается отдельно его API idempotency.
iGaming: accepted bet + event intent, затем idempotent player-history projection. Payout consumer должен atomically фиксировать posting и dedup identity в ledger boundary либо использовать поддержанный ledger API с business key.
13. Типичные ошибки проектирования¶
- Помечать sent до broker acknowledgment.
- Записывать inbox и business update разными transactions.
- Генерировать новый event_id при relay retry.
- Путать request idempotency с downstream event dedup.
- Обещать strict order только по auto-increment/created_at.
- Удалять inbox раньше допустимого replay.
14. Вопросы технического интервью с ответами¶
Почему duplicates остаются? Между broker accept и relay progress commit есть окно неопределённости; безопасный retry повторяет intent.
Достаточно ли inbox для HTTP payout? Нет, remote effect вне локальной транзакции. Нужны provider idempotency и сверка unknown outcomes.
Можно ли использовать CDC без polling? Да, CDC outbox — способ доставки committed intents, но retention, restart и consumer semantics всё равно проектируются.
15. Практическое упражнение¶
HYPOTHETICAL: составьте crash matrix для producer commit, broker accept, relay marker, inbox insert, business update, consumer commit и offset commit.
Критерий: нет committed business row без durable intent; после любого повторения остаётся один consumer DB effect. Затем замените DB effect на внешний payout и явно покажите, в какой строке матрицы прежнего доказательства уже недостаточно. Код и integration checks относятся к Этапу 7.
Дополнительные источники¶
Debezium 3.3 Outbox; Shopify; BEC slides; Kafka 4.1 external systems.