0% прочитано

Коли одна транзакція краща за подію

Подія корисна, коли на вже завершений факт можна відреагувати пізніше. Але якщо дві зміни разом утворюють одну правильну операцію, а проміжний стан недопустимий, одна транзакція часто є простішою і надійнішою межею.

5 жовтня 2026 р. 8 хв читанняЛаравель

Події добре допомагають розділяти відповідальність між частинами системи.

Але сама можливість створити подію ще не означає, що операцію варто розривати на дві частини.

Іноді друга зміна не є реакцією на першу. Вона є обов’язковою частиною тієї самої операції.

У такому випадку між першою і другою зміною може виникнути стан, який система взагалі не повинна дозволяти.

Тоді для мене головне питання звучить так: чи залишається стан правильним, якщо друга дія виконається пізніше?

Якщо ні, одна транзакція часто є кращою межею, ніж асинхронна подія.

Якщо правильність першої зміни залежить від того, що друга теж відбудеться, я не вважаю другу зміну незалежним обробником події.

Я спочатку дивлюся на проміжний стан

Проміжний стан визначає межу
Крок A   |   vпроміжний стан   |   vКрок B  Питання: чи може системазалишитися між A і Bі все одно бути правильною?

Це для мене найпростіша перевірка.

Якщо після першої зміни система вже має повністю правильний стан, друга дія може бути окремою реакцією.

Якщо ж без другої зміни дані суперечать правилу, розрив між ними створює небезпечне вікно.

Подія створює проміжок між двома змінами

Дві окремі точки збереження
Транзакція 1 змінити Acommit   |   vподія   |   vчерга   |   vобробник   |   vТранзакція 2

Між двома транзакціями може пройти мілісекунда, секунда або значно більше.

Обробник може бути відкладений, повторений або тимчасово завершитися помилкою.

Якщо стан після першого commit вже правильний, це нормально.

Але якщо він не повинен існувати без другої зміни, така архітектура переносить проблему узгодженості в чергу.

Одна транзакція прибирає некоректний стан назовні

PHP - Спрощена атомарна зміна
DB::transaction(function () use (    $firstId,    $secondId,): void {    $first = FirstRecord::query()        ->whereKey($firstId)        ->lockForUpdate()        ->firstOrFail();     $second = SecondRecord::query()        ->whereKey($secondId)        ->lockForUpdate()        ->firstOrFail();     $first->applyChange();    $second->applyRelatedChange();

У такому варіанті обидві зміни мають одну точку завершення.

Або вони обидві потрапляють у базу, або жодна не стає видимою як завершена операція.

Проміжний стан може існувати всередині виконання, але він не стає окремим збереженим станом системи.

Подія не є заміною атомарності

Подія добре відповідає на питання "хто ще хоче відреагувати на вже завершений факт?".

Транзакція відповідає на інше питання: "які зміни повинні стати видимими разом?".

Ці механізми не конкурують між собою і не замінюють один одного.

Проблема починається, коли подію використовують не для реакції на факт, а для завершення самого факту.

Я уявляю, що друга частина не виконається взагалі

Перевірка на повну відсутність другої реакції
Крок A завершено Крок B не виконався і більше ніколине виконається  Чи залишається системав допустимому стані?  YES-> подія може бути нормальною  

Я спеціально беру не оптимістичний сценарій із невеликою затримкою, а найгірший: друга дія не відбудеться взагалі.

Якщо після цього дані все одно правильні, асинхронна реакція може бути прийнятною.

Якщо ж система залишається в стані, який доведеться "долікувати" пізніше, це вже не просто реакція на подію.

Проміжний стан можуть побачити інші запити

Проміжний стан стає реальним станом системи
Запит A change Acommit   |   |------ проміжок ------|                         |                      change B  Запит B         read          |          vбачить стан

Це важливо не тільки для самої черги.

Інший HTTP-запит, фонове завдання або звіт може прочитати дані саме між двома змінами.

Якщо цей стан некоректний, він уже впливає на інші рішення системи.

Одна транзакція не дозволяє такому стану стати окремою завершеною версією даних.

Розділення подією додає окрему проблему відновлення

Щойно друга частина виконується окремо, потрібно відповісти на нові питання.

  • Що робити, якщо обробник завершився помилкою?
  • Скільки разів його повторювати?
  • Чи безпечний повтор?
  • Як знайти операції, які залишилися наполовину завершеними?
  • Хто відповідає за відновлення?

Іноді ця складність виправдана. Але я не додаю її, якщо одна локальна транзакція природно закриває правило.

Одна транзакція теж не є універсальною відповіддю

Цей принцип не означає, що все потрібно виконувати синхронно.

Якщо друга дія є повідомленням, аналітикою, оновленням окремої проєкції або іншою реакцією, без якої головний стан уже правильний, подія може бути кращою.

Так само одна база даних не може дати звичайну локальну транзакцію на кілька незалежних зовнішніх систем.

Тому я вибираю транзакцію не через любов до синхронного коду, а коли сама модель даних вимагає спільної атомарності.

Одна транзакція особливо природна в межах одного сховища

Найпростіший випадок - дві пов’язані зміни живуть у тій самій базі даних.

Якщо вони разом утворюють один інваріант, база вже має механізм, який дозволяє зафіксувати їх атомарно.

У такій ситуації подія між двома локальними записами іноді лише ускладнює те, що транзакція вирішує напряму.

Конкурентні зміни можуть вимагати не тільки транзакції, а й блокування

Сама транзакція не означає, що два конкурентні запити автоматично побачать потрібний порядок.

Якщо рішення залежить від актуального стану запису, я повторно читаю його всередині транзакції і за потреби використовую lockForUpdate().

Тобто одна транзакція визначає атомарну межу, а блокування допомагає захистити критичне рішення від конкурентної зміни.

Подія може з’явитися вже після завершення атомарної зміни

PHP - Спочатку завершити правило, потім повідомити про факт
DB::transaction(function () use ($id): void {    $record = Record::query()        ->whereKey($id)        ->lockForUpdate()        ->firstOrFail();     $record->applyPrimaryChange();    $record->applyRequiredRelatedChange();     $record->save();}); event(    new OperationCompleted($id),);

Вибір транзакції не означає відмову від подій.

Подія може бути дуже доречною після того, як обидві обов’язкові зміни вже успішно зафіксовані.

Тоді вона справді описує завершений факт, а не намагається завершити незакінчену операцію.

У реальному коді для такої публікації я також враховую межу commit, щоб не повідомити про факт, який ще може бути відкочений.

Мій простий вибір між транзакцією і подією

Критерій вибору
Друга зміна обов'язкова,щоб перша була правильною? YES | vЧи можна виконати їхатомарно в одному сховищі? YES | vодна транзакція  Основний стан уже правильний

Що я перевіряю перед тим, як розділити операцію подією

Перед таким рішенням я ставлю кілька питань:

  • Чи правильний стан після першої зміни без другої?
  • Що буде, якщо друга дія не виконається взагалі?
  • Чи можуть інші запити побачити небезпечний проміжний стан?
  • Чи дві зміни разом захищають одне правило?
  • Чи знаходяться обидві зміни в одному транзакційному сховищі?
  • Чи потрібне блокування через конкурентний доступ?
  • Чи додає подія окрему проблему повторів і відновлення?
  • Чи може подія бути опублікована вже після завершення атомарної зміни?

Спочатку я завершую інваріант, а вже потім розділяю реакції

Події дуже корисні, коли система вже має завершений факт і кілька незалежних реакцій на нього.

Але якщо дві зміни разом утворюють одну правильну операцію, я не розриваю їх лише для того, щоб зробити компоненти менш пов’язаними.

Некоректний проміжний стан, повтори обробника і механізм відновлення можуть створити значно складнішу систему, ніж одна локальна транзакція.

Тому спочатку я визначаю атомарну межу правил даних.

І лише після того, як система має коректний завершений стан, вирішую, які наступні реакції можна безпечно винести в події.

Якщо між двома змінами система не повинна існувати, я намагаюся не створювати цей проміжок архітектурно.

Подія добре реагує на завершений факт. Вона не повинна бути обов’язковою другою половиною факту, який ще не завершений.