Short answer: Транзакционный outbox превращает хрупкие встроенные вызовы API в надёжные и аудируемые доставки: события фиксируются в той же транзакции базы данных, что и доменная запись, а отправляются асинхронно. Добавление транзакционного outbox в vibecoded‑приложение предотвращает потерю уведомлений и двойные побочные эффекты при сбоях процессов или повторных попытках. Шаблон даёт доставку как минимум один раз, позволяет идемпотентным потребителям дедуплицировать и изолирует латентность и отказы от пути обработки запроса. Для большинства прототипов с платежами, email, вебхуками и другими внешними побочными эффектами транзакционный outbox — самый быстрый путь к надёжности в проде. Вам не нужна очередь на старте: одной таблицы и процесса‑диспетчера достаточно, чтобы получить большую часть выгоды. Паттерн outbox также создаёт чистую грань для мониторинга, повторов и эволюции схемы по мере взросления продукта.

Key takeaways

  • Транзакционный outbox сохраняет событие в той же транзакции, что и бизнес‑изменение, и затем отправляет его асинхронно, предотвращая потерю сообщений.
  • Шаблон обеспечивает доставку как минимум один раз; объединяйте его с идемпотентными ключами и дедупликацией для безопасных внешних побочных эффектов.
  • Outbox можно реализовать одной таблицей и поллинг‑диспетчером; позже перейти на change data capture (CDC) без изменений у производителей.
  • Переходите от встроенных вызовов к outbox за фичефлагом и выкатывайте постепенно; держите двойную запись для чтений, чтобы проверить паритет перед переключением потребителей.
  • Наблюдаемость, бэкпрешер и обработка dead‑letter превращают прототип в управляемую систему; относитесь к outbox как к первоклассному подсистемному компоненту.

What is a transactional outbox?

Транзакционный outbox — это паттерн надёжности, при котором исходящее событие фиксируется в той же транзакции БД, что и доменное изменение, а затем доставляется асинхронно. Запись в outbox — это источник истины о том, что нужно доставить; после коммита транзакции её не потеряют ни сбои, ни перезапуски, ни таймауты.

Без outbox прототипы часто вызывают API сразу после записи в БД. Если вызов падает или процесс умирает, вы получаете закоммиченные данные без побочного эффекта — или дублированный эффект из‑за ретраев. Outbox разделяет корректность данных и доставку и позволяет рассуждать о них по отдельности.

Ключевые свойства:

  • Атомарное создание: запись outbox и доменная запись коммитятся вместе.
  • Асинхронная отправка: воркер читает outbox, вызывает внешние системы, помечает успех и фиксирует попытки.
  • Как минимум один раз: ретраи гарантируют попытки доставки; потребители должны быть идемпотентными.
  • Аудируемость: можно запросить, что отправлялось, когда и сколько раз.

When should a vibecoded app add a transactional outbox?

Добавляйте транзакционный outbox, как только прототип вызывает внешние побочные эффекты, которые нельзя терять или дублировать. Встроенные вызовы внутри обработчика запроса годятся для демо; в проде нужны изоляция и восстановление.

Понятные сигналы, что нужен outbox:

  • Списания платежей, возвраты или проводки в учёте после обновлений заказа.
  • Email или SMS, привязанные к изменениям состояния (регистрация, сброс пароля, статусы доставки).
  • Вебхуки в системы клиентов или партнёров.
  • Интеграции, требующие ретраев с backoff, идемпотентности или строгих лимитов скорости.
  • Медленные сторонние вызовы, вызывающие таймауты или высокую хвостовую латентность пользовательских запросов.

Даже если позже вы планируете брокер сообщений, начните с транзакционного outbox. Вы получите стабильный контракт производителя, проверяемое поведение и наблюдаемость, которые поедут с вами дальше.

How do you design the outbox table and event schema?

Проектируйте модель данных так, чтобы производителям было просто, а диспетчеру хватало всего для безопасной доставки. Не переусложняйте нормализацией; диспетчеру нужен самодостаточный payload и чёткая метаинформация.

Рекомендуемые поля:

  • id: монотонно растущий первичный ключ или ULID; также ваш ключ идемпотентности для состояния доставки.
  • topic/type: стабильное имя события, например order.created.
  • aggregate_id: идентификатор доменной сущности для упорядочивания и партиционирования.
  • payload: JSON или бинарный payload, с версией в schema_version.
  • created_at: время создания; используется для SLA и окон бэкфилла.
  • attempts, last_attempt_at, next_attempt_at: для планирования ретраев и экспоненциального backoff.
  • status: pending, delivering, delivered, failed, dead_letter.
  • error: последняя строка/код ошибки для триажа.
  • tenant_id (в мульти‑тенант среде): включает троттлинг и изоляцию по арендаторам.

Рекомендации по схеме:

  • Встраивайте минимальные, неизменяемые факты, нужные потребителю; избегайте доп‑запросов состояния в диспетчере.
  • Добавьте явные event_id и idempotency_key в payload для downstream‑дедупликации.
  • Версионируйте payload’ы; флаг schema_version позволяет мягко обновляться.
  • Предпочитайте аддитивные изменения; держите старые поля, пока получатели не мигрируют.

How do you implement the dispatcher: polling vs change data capture?

Доставлять из outbox можно периодическим поллером или через change data capture (CDC). Начните с поллера; переходите на CDC при необходимости низкой латентности или высокой пропускной способности.

Polling dispatcher

Поллинг‑диспетчер просыпается по интервалу, выбирает партию ожидающих записей, помечает их delivering, отправляет и обновляет статус. Это подходит большинству MVP и просто в эксплуатации.

  1. SELECT партию с FOR UPDATE SKIP LOCKED (или аналогом), чтобы избежать конкуренции воркеров.
  2. Пометьте выбранные записи как delivering и установите таймаут видимости.
  3. Отправьте payload в цель (HTTP, очередь, email‑провайдер).
  4. При успехе пометьте delivered и сохраните квитанцию, если доступна.
  5. При неудаче увеличьте attempts, вычислите next_attempt_at с backoff и джиттером и залогируйте детали ошибки.

Плюсы: минимум инфраструктуры, прозрачное поведение, легко дебажить. Минусы: латентность ограничена интервалом опроса, возможны N+1 запросы при наивной реализации.

Change data capture (CDC)

CDC транслирует новые строки outbox как журнал изменений в потребителя (например, через логическую репликацию БД или чтение binlog). Диспетчер обрабатывает события почти в реальном времени и масштабируется горизонтально по ключу партиции.

Плюсы: низкая латентность, высокая пропускная способность, естественное партиционирование. Минусы: больше движущихся частей, операционные накладные, провайдер‑специфичные нюансы.

Выбирайте CDC, когда нужна субсекундная доставка, несколько даунстримов или фановая рассылка событий в стрим‑процессор. Держите контракт производителя неизменным, чтобы мигрировать диспетчеры без правок в приложении.

How do you guarantee delivery without duplicates?

Паттерн outbox гарантирует доставку как минимум один раз, а не строго «ровно один раз». Эффективно‑однократного результата достигают сочетанием ретраев с идемпотентностью у получателя.

Гарантии производителя

  • Атомарная запись: доменная запись и вставка в outbox коммитятся одной транзакцией.
  • Единственная ответственность: производители ничего не доставляют; они только создают строки в outbox.
  • Монотонный id: используйте id outbox как стабильный токен порядка по агрегату или партиции.

Гарантии диспетчера

  • Таймауты видимости: предотвращают залипание доставок; неподтверждённые элементы снова становятся доступны.
  • Backoff и джиттер: избегают горячих циклов и лавинных ретраев после простоев.
  • Обработка «ядовитых» сообщений: после порога переносите в dead_letter для ручной или автоматической ремедиации.

Гарантии потребителя

  • Ключи идемпотентности: включайте event_id или составной ключ (topic + aggregate_id + version), чтобы потребители безопасно дедуплицировали.
  • Идемпотентность побочных эффектов: провайдер платежей, отправка email или приёмник вебхука должны игнорировать повторы; храните обработанные ключи с TTL при необходимости.
  • Учет порядка: если порядок важен по агрегату, обрабатывайте по partition aggregate_id и держите только один запрос в полёте на ключ.

Для HTTP‑целей согласуйте поведение диспетчера с надёжными практиками клиента. Мы описали таймауты, ретраи и предохранители в HTTP Timeouts and Retries for Vibecoded Apps; применяйте эти политики внутри диспетчера для исходящих вызовов.

How do you migrate from inline calls to a transactional outbox safely?

Мигрируйте поэтапно, чтобы не рисковать простоем в проде. Цель — заставить производителей писать строки outbox, сохраняя легаси‑доставку активной, а затем переключать маршруты на диспетчер.

  1. Внедрите схему outbox: напишите миграцию с прямой и обратной совместимостью; добавьте индексы на status, next_attempt_at и aggregate_id.
  2. Обёрните производителей: замените встроенные вызовы функцией, которая пишет запись outbox внутри существующей транзакции. Держите встроенный вызов за фичефлагом.
  3. Добавьте диспетчер: запустите воркер, который читает и доставляет записи outbox для небольшой части типов событий.
  4. Двойная доставка (опционально): какое‑то время оставьте встроенную доставку и гоняйте диспетчер в «тень» или на стейджинг‑цель, чтобы сверить паритет.
  5. Переключение: выключите встроенные вызовы фичефлагом и сделайте outbox единственным путём доставки для этого типа событий.
  6. Бэкфилл: при разрывах — засеять outbox из авторитетных таблиц для безопасного окна повторов.

Используйте фичефлаги, чтобы стадировать переключение по типу события, арендатору или региону. Мы разбираем практические паттерны переключений в Feature Flags for MVP, но можно обойтись и простым конфиг‑гейтом на маршрут.

What about observability, backpressure, and dead letters?

Относитесь к outbox как к подсистеме со своими SLO. В проде важнее не «счастливый путь», а то, что вы сделаете при стопорах.

Основы наблюдаемости:

  • Метрики: глубина очереди по топику, возраст старейшего pending, скорость доставки, доля ошибок по целям, ретраи по корзинам попыток, dead_letter в час.
  • Логи: структурированные записи с event_id, целью, номером попытки, латентностью, кодом статуса и нормализованной причиной ошибки.
  • Трейсинг: свяжите исходный спан запроса с записью в outbox и спаном доставки; прокидывайте trace‑id в заголовках вниз по цепочке, где возможно.
  • Дашборды/алерты: алертите по возрасту бэклога, устойчивым non‑2xx для цели и застрявшим «delivering» дольше таймаута видимости.

Бэкпрешер и справедливость:

  • Ограничивайте конкуренцию по целям, чтобы не перегружать провайдеров.
  • Троттльте по арендаторам в мульти‑тенант среде, чтобы «горячий» арендатор не голодал остальных.
  • Используйте экспоненциальный backoff с джиттером и верхней границей; согласуйте с лимитами провайдера.

Dead letters:

  • Перемещайте события в статус dead_letter после ограниченного числа попыток или постоянных ошибок (например, семантические 4xx).
  • Дайте операторский инструмент для просмотра payload’ов, правки при необходимости и повторной отправки или удаления с указанием причины.
  • Для чувствительных данных обеспечьте, чтобы dead letters соблюдали политику хранения и редактирования ПДн.

Если ваш outbox доставляет клиентские вебхуки, относитесь к проверке подписи и обработке повторов как к первоклассным задачам. Наш гид Webhook Signature Verification покрывает паттерны на стороне приёмника, которых вы должны ждать и тестировать.

How do you evolve event schemas without breaking consumers?

Эволюция схем важна, как только у вас больше одного потребителя или вы отдаёте события клиентам. Безопасный подход — аддитивный, версионированный, обратимый.

  • Встраивайте schema_version в каждый payload; начните с 1 и увеличивайте только при ломающих изменениях.
  • Предпочитайте добавление полей; не удаляйте и не переименовывайте, пока все потребители не подтвердят поддержку.
  • Документируйте контракты: смысл полей, допустимые значения, nullability и примеры payload’ов по версиям.
  • Гоняйте контракт‑тесты в CI для диспетчера и критичных потребителей.
  • Ограждайте новые версии фичефлагом и раскатывайте сперва на часть арендаторов или маршрутов.

Если ваш outbox фан-аутит на несколько даунстримов, ведите конфигурацию по подписчику — версия и фильтры. Когда нужен жёсткий разрыв, публикуйте новый топик и депрекируйте старый: версионирование топиков лучше скрытых изменений payload’ов.

How do you keep ordering without killing throughput?

Порядок важен только в рамках доменных границ, редко глобально. Определите, где он нужен, и партиционируйте работу соответственно.

  • Порядок по агрегату: обеспечивайте один запрос «в полёте» на aggregate_id; держите небольшой пул воркеров на ключ.
  • Партиционированная конкуренция: хэшируйте aggregate_id в N партиций; запускайте по воркеру на партицию для упорядоченной доставки.
  • Пересборка у потребителя: при небольшой расстановке не по порядку потребители могут буферизовать по ключу короткое окно и применять по порядку, когда возможно.
  • Толерантный к порядку дизайн: делайте потребителей коммутативными или идемпотентными, чтобы снизить потребность в строгой последовательности.

Не сериализуйте весь outbox. Вы создадите узкое место в один файл, убив пропускную способность и устойчивость.

What clean-up, storage, and privacy policies apply to an outbox?

Таблицы outbox растут. Планируйте ретеншн, редактирование и архивирование с начала, чтобы не обнаружить 100‑миллионную таблицу за неделю до релиза.

  • Ретеншн: храните доставленные строки ограниченное окно; чистите по расписанию или переносите в архивную таблицу.
  • ПДн: не храните голые секреты, токены и лишние персональные данные в payload’ах; по умолчанию редактируйте логи.
  • Компакция: если часто эмитите замещающие события по одному агрегату, рассмотрите периодическую компакцию для аналитики, но никогда не удаляйте недоставленные события.
  • Индексы: пересматривайте индексы по мере роста; распространены составные (status, next_attempt_at) и (aggregate_id, status).

What tests make a transactional outbox production-ready?

Тестируйте стык, а не только «счастливый путь». Цель — доказать атомарность, поведение ретраев и идемпотентность при реалистичных сбоях.

  • Тест атомарности: форсируйте крэш между доменной записью и вставкой в outbox; убедитесь, что они вместе либо успешны, либо откатываются.
  • Тест ретраев: симулируйте сетевые ошибки и 5xx; проверьте backoff и итоговый успех.
  • Подавление дублей: внедрите дублирующую доставку; проверьте, что потребитель обрабатывает ключ идемпотентности без побочных эффектов.
  • Тест порядка: сгенерируйте несколько событий для одного агрегата; проверьте упорядочивание по ключу при конкуренции.
  • Путь в dead letter: возвращайте стабильные 4xx; проверьте переход в dead_letter и работу операторского инструмента.
  • Нагрузочный: измерьте рост бэклога на пике; настройте размеры батчей, конкуренцию и запросы к БД.

Example: refactoring a vibecoded inline call to an outbox

Предположим, ваш обработчик регистрации пишет пользователя, а затем вызывает email API «в линию». Замените встроенный вызов функцией‑производителем, которая вставляет событие email.sign_up в outbox в рамках той же транзакции. Диспетчер опрашивает ожидающие строки и отправляет письмо асинхронно с ретраями и идемпотентностью.

  1. Внутри транзакции регистрации: вставьте пользователя, вставьте строку outbox для email.sign_up с event_id и payload { user_id, email }.
  2. Диспетчер выбирает батч ожидающих строк, ставит delivering с таймаутом видимости.
  3. Пытается отправить через провайдера; при 2xx помечает delivered; при 5xx или таймауте — backoff; при 4xx — dead letter.
  4. Потребитель (email‑провайдер или ваш враппер) использует event_id как ключ идемпотентности, чтобы избежать дублей.

Этот рефактор убирает email‑провайдера из критического пути, снижает P95 регистрации и делает сбои видимыми и восстанавливаемыми.

How Moai Team approaches this

Мы закрываем разрыв «vibecoding → продакшен», вводя транзакционный outbox как наименьший надёжный «хребет» интеграций. Встраиваемся в вашу кодовую базу, добавляем схему outbox и обёртки производителей в существующие транзакции и поднимаем диспетчер, который уважает таймауты, ретраи и идемпотентность. Проектируем троттлинг по арендаторам, партиционирование и наблюдаемость, чтобы вы масштабировались без переделок. Делаем переключение за фичефлагом, шедоуим трафик при необходимости и доказываем восстановление, репетируя сбои до запуска.

Когда вы будете готовы к CDC или шине сообщений, мы держим контракт производителя стабильным и с уверенностью меняем диспетчеры. Мы также выравниваем доставку вебхуков с проверкой подписи и вменяемыми политиками ретраев, опираясь на практики из Webhook Signature Verification и HTTP Timeouts and Retries.

Frequently Asked Questions

Is a transactional outbox overkill for an MVP?

Нет. Транзакционный outbox — небольшое изменение с огромной выгодой по надёжности. Он убирает хрупкие встроенные вызовы из путей запросов и даёт повторы, аудит и наблюдаемость. Реализуется одной таблицей и лёгким воркером, а позже можно вырасти до CDC.

Do I still need a message queue if I use a transactional outbox?

Не на старте. Поллинг‑диспетчер покрывает большинство нужд MVP. По мере роста пропускной способности или фан‑аута можно доставлять из outbox в очередь или стрим без изменений у производителей. Outbox остаётся авторитетной записью того, что нужно доставить.

Can a transactional outbox guarantee exactly-once delivery?

Нет. Паттерн даёт доставку как минимум один раз с высокой практической безопасностью при идемпотентных потребителях. Используйте ключи идемпотентности, хранилища дедупликации и коммутативные побочные эффекты, чтобы добиться «эффективно один раз».

How do I keep event ordering?

Требуйте порядок только там, где он нужен домену, обычно по агрегату. Партиционируйте по aggregate_id и обрабатывайте по одному событию в полёте на ключ. Если возможна небольшая расстановка не по порядку, коротко буферизуйте у потребителя или делайте его идемпотентным и толерантным к порядку.

What happens if the dispatcher crashes mid-delivery?

Используйте таймаут видимости для delivering‑строк. Если воркер упадёт, блок истечёт, и другой воркер безопасно повторит попытку. Повторы безопасны, потому что потребители дедуплицируют по ключу идемпотентности.

How do I prevent the outbox table from growing forever?

Применяйте политику ретеншна: чистите доставленные строки после безопасного окна или архивируйте их. Индексируйте по status и next_attempt_at для эффективных сканов. Редактируйте или не храните чувствительные поля в payload’ах и следите, чтобы dead letters соблюдали правила хранения данных.

Нужна команда, которая внедрит транзакционный outbox без паузы в фичах? Свяжитесь с нами: Moai Team — свяжитесь с нами.