Transactional Outbox vs Dual Write¶
Тип материала: инженерное сравнение; выводы зависят от требований и условий эксплуатации. Схемы и примеры иллюстративны.
INFERENCE: сравнивается обычная независимая запись business DB и broker без общей transaction. Термин dual write также встречается в migration protocols с дополнительными проверками; сам по себе он не описывает достаточную гарантию. Сценарии и схема — HYPOTHETICAL.
Failure matrix и критерии¶
| Критерий | Independent dual write | Transactional Outbox |
|---|---|---|
| DB commit и delivery intent | Два независимых действия | Одна локальная DB transaction |
| Crash между ними | Lost event либо ghost event | Intent сохраняется вместе с business state |
| Broker outage | API либо падает, либо оставляет gap | Business commit возможен, outbox backlog растёт |
| Дубли | Возможны при retries | Ожидаемы у relay; consumer дедуплицирует |
| Ordering | Не гарантируется порядком строк кода | Также требует явного relay/key/version protocol |
| Эксплуатация | Мало компонентов, сложный repair | Relay, cleanup, lag и inbox/idempotency |
| Когда допустимо | Разрешённая lossiness или отдельный доказанный protocol | Нужна durable event intent вместе с commit |
DB-first: payment committed, publish не состоялся — downstream ничего не знает. Publish-first: event ушёл, DB rollback — downstream верит в несуществующую операцию. Обработка exception не закрывает crash или потерянный response.
Выбранная граница¶
flowchart TB
C["Command"] --> T["Одна DB transaction"]
T --> B["Business rows"]
T --> O["Outbox intent"]
O --> R["Relay: publish и retry"]
R --> K["Broker"]
K --> I["Consumer: inbox + effect atomically"]
Outbox закрывает atomicity business+intent, но не объединяет producer и consumer databases. Consumer DB effect требует своего boundary. External HTTP effect требует provider idempotency либо иной protocol и reconciliation.
Конкретные сценарии¶
PaymentCaptured → reporting: потерянный intent нарушает полноту отчёта. Outbox с durable relay и consumer dedup оправдан. Суммы и IDs сверяются с ledger.
BetAccepted → history/risk: accepted bet должен иметь устойчивый integration event. Outbox можно читать CDC; consumer проверяет revision, чтобы stale update не перезаписал corrected bet.
Disposable telemetry: если бизнес явно допускает отдельные потери и определена observability, best-effort publish может быть осознанным выбором. Нельзя переносить его на financial postings.
Online migration: dual writes внутри контролируемой миграции требуют backfill, сравнения, rollback и знания transaction scope. FACT: Stripe описал staged online migration в 2017 году. Stripe. Это не доказательство безопасности любого произвольного DB+broker dual write.
Ограничения¶
Гарантия доставки условна: outbox не должен быть удалён до подтверждённого transfer, relay должен восстанавливаться, broker — сохранять input по заданному contract. «Intent durable» не равно «job успешно завершится».
Для transactional broker + совместимой external DB можно рассмотреть coordination/2PC, но это отдельная технология с recovery costs. Producer idempotence внутри broker не атомизирует запись в чужой DB.
FACT: Shopify объясняет staged job queue и повторную постановку после ошибки progress update. Shopify. Debezium предоставляет router, но приложение само создаёт atomic outbox. Debezium 3.3.
Типичные ошибки проектирования¶
Пометить sent до broker ack; считать inbox достаточным для payout API; выдавать transaction broker за SQL+broker atomicity; обещать delivery при бесконтрольном cleanup.
Вопросы на собеседовании¶
Почему retry publish недостаточен? После crash intent может не сохраниться, а успешный publish с потерянным ответом даст duplicate.
Зачем inbox при outbox? Relay at-least-once оставляет окно повторной доставки; producer atomic intent не решает consumer effect.
Практические упражнения¶
Нарисуйте два dual-write порядка и вставьте crash между действиями. Затем покажите outbox protocol и crash после broker accept. Ожидается, что потеря превращается в безопасно обнаруживаемый повтор, а не исчезают вообще все failure modes.