0% прочитано

Коли подія справді доречна

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

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

Події дуже легко почати використовувати занадто часто.

Змінився статус - створити подію. Зберігся запис - ще одна подія. Виконався метод - можливо, теж подія.

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

Для мене важливіше інше питання: чи основна операція вже завершена правильно без майбутнього обробника?

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

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

Спочатку я визначаю, де закінчується основна операція

Основна зміна і реакції після неї
Основна операція    |    +--> перевірити правила    +--> змінити стан    +--> зберегти результат    |    vоперація завершена  Після цього:     +--> аудит    +--> повідомлення    +--> проєкція    +--> аналітика

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

Наприклад, після успішної зміни може знадобитися:

  • створити запис для аудиту;
  • надіслати повідомлення;
  • оновити окрему проєкцію для читання;
  • передати дані в аналітику;
  • запустити другорядну фонову роботу.

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

Хороша подія описує факт, який уже стався

PHP - Подія описує завершений факт
final readonly class OrderConfirmed{    public function __construct(        public string $orderId,        public DateTimeImmutable $confirmedAt,    ) {}}

Мені подобається називати події в минулому часі: OrderConfirmed, ReportGenerated, ProfileUpdated.

Так назва сама підказує семантику: це не наказ щось зробити, а повідомлення про факт, який уже відбувся.

Обробники можуть вирішити, чи потрібно на цей факт реагувати.

Подія зменшує залежність від кількості реакцій

PHP - Основний сценарій знає про всі реакції
$order->confirm(); $auditService->recordConfirmation($order);$notificationService->sendConfirmation($order);$analyticsService->trackConfirmation($order);$projectionService->refresh($order);
PHP - Основний сценарій повідомляє лише про факт
$order->confirm(); $repository->save($order); event(new OrderConfirmed(    $order->id(),    $order->confirmedAt(),));

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

Додався новий канал повідомлень - змінюємо основний сценарій. З’явилась нова проєкція - знову змінюємо його.

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

Мій головний тест: чи допустима затримка?

Перевірка допустимої затримки
Основна зміна завершена        |        vподія опублікована        |        +--> обробник через 10 ms        |        +--> обробник через 2 s        |        +--> повторна спроба через 30 s  Чи залишається основний стан правильним? YES -> подія може бути доречною 

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

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

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

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

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

Є кілька типів реакцій, для яких події часто природні.

Аудит. Основна зміна вже відбулася. Окремий механізм може зафіксувати факт для історії.

Повідомлення. Успішна бізнес-операція не повинна ставати невдалою лише тому, що поштовий сервіс відповів повільно.

Проєкції для читання. Окреме представлення можна оновити після зміни основних даних, якщо коротка затримка допустима.

Аналітика та метрики. Вони реагують на факт, але не визначають, чи сам факт був дозволений.

Подія погано підходить для другої обов’язкової половини правила

Не кожну другу дію варто переносити в обробник
Крок A збережено    |    vподія    |    vКрок B ще не виконано  Якщо стан між A і B недопустимий,це небезпечна межа.

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

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

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

Подія не повинна повідомляти про те, що ще може відкотитися

PHP - Подія може піти раніше за остаточний commit
DB::transaction(function () use ($order): void {    $order->confirm();     event(new OrderConfirmed(        $order->id(),        $order->confirmedAt(),    ));     $repository->save($order);});

Є ще одна важлива межа: подія повинна описувати факт, який справді стався.

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

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

PHP - Подія після успішного commit
DB::transaction(function () use ($order): void {    $order->confirm();     $repository->save($order);     DB::afterCommit(        static fn () => event(            new OrderConfirmed(                $order->id(),                $order->confirmedAt(),            )        )    );});

afterCommit вирішує порядок, але не всі проблеми доставки

afterCommit допомагає не опублікувати подію до успішного збереження даних.

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

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

Якщо втрата такої події критична, потрібна сильніша гарантія.

Для важливої доставки я розглядаю outbox

Основний факт і запис події фіксуються разом
Одна транзакція:     основна зміна          +    запис події в outbox          |        COMMIT Після commit:     worker читає outbox          |          v    доставляє подію          |          v

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

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

Окремий процес потім читає ці записи і доставляє повідомлення.

Це складніше за звичайний event(), тому я не додаю outbox автоматично всюди. Але для важливих інтеграційних подій така гарантія може бути виправданою.

Обробник події повинен бути готовий до повтору

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

Завдання може повторитися після помилки. Повідомлення може бути доставлене повторно. Worker може завершити роботу після виконання дії, але до підтвердження успіху.

Тому важливі обробники я намагаюся робити ідемпотентними: повтор тієї самої події не повинен створювати другий однаковий побічний ефект.

У подію я передаю факт, а не весь Eloquent model

PHP - Невеликий явний payload
final readonly class OrderConfirmed{    public function __construct(        public string $orderId,        public DateTimeImmutable $confirmedAt,    ) {}}

Події також легко перетворити на прихований спосіб передавання великого графа об’єктів.

Я намагаюся цього уникати.

Обробнику зазвичай потрібні кілька стабільних фактів: ідентифікатор, час події та дані, без яких сама подія втратить зміст.

Це робить контракт події явнішим і зменшує залежність від поточного стану ORM-моделі.

Не кожна внутрішня подія повинна ставати зовнішньою

Ще одна межа, яку я не змішую: внутрішня подія застосунку і контракт для інших систем.

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

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

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

Що я перевіряю перед додаванням події

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

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

Подія доречна там, де факт уже завершений

Для мене подія не є способом зробити код "більш архітектурним".

Вона корисна тоді, коли допомагає правильно розділити основний факт і реакції на нього.

Основна операція повинна завершити свої правила та зберегти коректний стан.

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

Для простих випадків достатньо публікації після commit. Для важливої гарантованої доставки може знадобитися outbox. Обробники повинні враховувати повтори.

Головний критерій для мене простий: якщо подія затримається, основний факт усе одно повинен залишатися правильним і завершеним.

Подія доречна не тому, що щось змінилося, а тому, що завершений факт уже може мати незалежних слухачів.