Проблема подвійного запису. Сервіс має зробити дві речі: зберегти зміну в базі і опублікувати подію в брокер повідомлень. Це дві різні системи, і атомарно зробити обидві дії неможливо:
DB::transaction(fn () => $order->markPaid());
$broker->publish(new OrderPaid($order->id)); // а якщо брокер недоступний чи процес впав тут?
- база оновилася, подія не опублікована - інші сервіси ніколи не дізнаються про оплату;
- якщо спершу публікувати, а потім зберігати, - подія є, а зміна в базі відкотилася.
Transactional Outbox: подія записується в таблицю тієї ж бази, у тій самій транзакції, що й зміна даних:
DB::transaction(function () use ($order) {
$order->markPaid();
DB::table('outbox')->insert([
'id' => (string) Str::uuid7(),
'type' => 'order.paid',
'payload' => json_encode(['order_id' => $order->id]),
'created_at' => now(),
]);
});
Тепер зміна і запис про подію або збережені обидва, або жоден.
Окремий процес-ретранслятор (relay) читає таблицю outbox і публікує записи в брокер, позначаючи опубліковані:
- опитування таблиці - простий варіант (команда в планувальнику чи постійний воркер);
- читання журналу змін бази (CDC, наприклад Debezium) - без навантаження опитуванням, з мінімальною затримкою.
Що варто врахувати:
- at-least-once: ретранслятор може опублікувати подію, впасти до позначки «опубліковано» і опублікувати знову. Споживачі мають бути ідемпотентними - ідентифікатор запису outbox стає ідентифікатором повідомлення;
- порядок: публікувати в порядку створення, якщо споживачам важлива послідовність;
- очищення: опубліковані записи видаляти чи архівувати;
- моніторинг затримки: записи, що висять неопублікованими, - сигнал проблеми з брокером чи ретранслятором.
Аналог у межах Laravel-застосунку: джоба, поставлена в чергу database у тій самій транзакції, або afterCommit() / ShouldDispatchAfterCommit - щоб подія й джоба відправлялися лише після успішного коміту. Але afterCommit не захищає від падіння процесу між комітом і відправкою в зовнішню чергу - Outbox захищає.
Дзеркальний патерн на стороні споживача - Inbox: записувати отримані повідомлення в таблицю для дедуплікації й надійної обробки.
Докладніше в документації: Microservices.io: Transactional Outbox