📬 Пост 51. Outbox Pattern - как не потерять события между БД и Kafka 😎

Представим обычный сценарий: пользователь создал заказ. Нам нужно сделать две вещи: 1. сохранить заказ в БД 2. отправить OrderCreated в Kafka И вот тут есть проблема. Если сделать просто: создали заказ в БД ↓ отправили событие в Kafka то между этими операциями есть окно для фейла. Например: БД: ✅ заказ сохранился -> сервис упал -> Kafka: ❌ событие не отправили В итоге заказ существует, а Payment Service, Notification Service и другие потребители вообще не знают о нём. И наоборот тоже может быть: Kafka: ✅ OrderCreated отправили БД: ❌ транзакция откатилась Теперь другие сервисы думают, что заказ создан, хотя в БД его нет.

💥 Вот эту проблему и решает Outbox Pattern.

🧠 Суть паттерна Мы не пытаемся одной операцией записать данные в БД и отправить сообщение в Kafka. Вместо этого сохраняем заказ и событие в одну транзакцию БД. Например:

BEGIN;

INSERT INTO orders ( id, user_id, amount, status ) VALUES ( 500, 42, 5000, 'NEW' );

INSERT INTO outbox ( id, event_type, aggregate_id, payload ) VALUES ( 'event-123', 'OrderCreated', '500', '{...}' );

COMMIT; Теперь у нас две таблицы: orders -> заказ создан outbox -> OrderCreated И они записаны одной транзакцией.

Поэтому не получится ситуации "заказ сохранили, а событие забыли".

📦 А как событие попадает в Kafka? Для этого есть отдельный процесс - Outbox Publisher. Он периодически забирает события из outbox: outbox -> берём NEW -> отправляем в Kafka -> получаем ACK -> помечаем SENT Например, в таблице: event-123 | OrderCreated | 500 | NEW После успешной отправки: event-123 | OrderCreated | 500 | SENT

А Kafka уже раздаёт событие нужным потребителям: Payment / Notification / Analytics

⚠️ Но тут есть важный нюанс Представим: 1. Publisher отправил OrderCreated в Kafka 2. Kafka ответила OK 3. Publisher упал 4. status = SENT сохранить не успел После перезапуска Publisher увидит: OrderCreated | NEW и отправит его ещё раз. Получится дубликат.

Поэтому Outbox обычно работает по принципу at-least-once delivery.

А значит, не producer управляет идемпотентностью, а consumer должен быть идемпотентным. Например, на консьюмере можно хранить event_id уже обработанных событий: event-123 → обработан ✅ Если event-123 прилетел второй раз, consumer понимает: "Ага, я это уже обрабатывал" и повторно бизнес-операцию не выполняет.

🎯 Итого Без Outbox: DB → Kafka и между ними возможна потеря события. С Outbox: DB ├── business data └── outbox event ↓ одна транзакция ↓ Outbox Publisher ↓ Kafka ↓ consumers

Главная идея Outbox: сначала надёжно сохранить бизнес-изменение и событие о нём в одной транзакции, а уже потом доставить событие в брокер. Именно поэтому паттерн так часто используют в микросервисах, где есть связка БД + Kafka/RabbitMQ. #outbox #kafka #архитектура #паттерны #интеграции #микросервисы #системныйанализ