Факт, на який хтось відреагує
Кандидат відкликав відгук. Роботодавцю потрібен лист, лічильнику відгуків мінус один, аналітиці запис у воронку. Якщо викликати все це з методу withdraw(), агрегат почне знати про пошту й звіти. З агрегат лише фіксує факт ApplicationWithdrawn, а реагують на нього окремо.
Подію називають дієсловом у минулому часі й мовою домену: ApplicationWithdrawn, а не SendWithdrawalEmail. Вона незмінна й несе лише потрібне: ID відгуку, час, хто це зробив. Агрегат складає події у список, а сценарій після збереження їх. Якщо подію читають інші контексти чи сервіси, її формат стає контрактом, і таку подію називають .
Найбільше помилок тут повʼязано з моментом публікації. Подію віддали до коміту, транзакція відкотилась, а лист уже пішов про те, чого не сталося. Тому події публікують після успішного збереження. Якщо жодна не має загубитися, їх пишуть в у тій самій транзакції, що й агрегат.
Агрегат записує, сценарій публікує
- WithdrawApplication використовує Application (withdraw()).
- Application створює ApplicationWithdrawn.
- WithdrawApplication використовує ApplicationRepository (save()).
- WithdrawApplication використовує Dispatcher (dispatch()).
- ApplicationWithdrawn надсилає подію, її отримує Dispatcher.
- Dispatcher надсилає подію, її отримує NotifyEmployer.
- Dispatcher надсилає подію, її отримує RefreshCounters.
- 1Назва в минулому часі й мовою домену: ApplicationWithdrawn, а не SendEmail.
- 2Агрегат записує подію, але не викликає обробників і нічого про них не знає.
- 3Публікують після коміту. Якщо втрата неприпустима, подію пишуть в outbox у тій самій транзакції.
- 4Обробник працює у власній транзакції, і його падіння не скасовує вже збережену зміну.
Що є в PHP Ukraine замість подій
Доменних подій, які один контекст публікує для іншого, у PHP Ukraine поки немає. Найближче до них історія відгуку: агрегат Application складає записи ApplicationEvent, а репозиторій пише їх у тій самій транзакції, що й сам відгук. Це журнал для людей, на нього ніхто не підписаний. Лист роботодавцю про новий відгук JobApplyController шле напряму, після сценарію ApplyToJob.
Як з подіями зазвичай помиляються
Подія як команда
SendWithdrawalEmail замість ApplicationWithdrawn. Агрегат вирішує, що робити далі, і кожна нова реакція змінює домен.
Натомість: називайте факт у минулому часі. Що з ним робити, вирішують обробники.
Публікація до коміту
Обробник уже надіслав лист, а транзакція потім відкотилась. Кандидат отримав повідомлення про зміну, якої в базі немає.
Натомість: віддавайте події після коміту. У Laravel є DB::afterCommit(), а для гарантованої доставки потрібен outbox.
Уся сутність у події
Подія несе весь обʼєкт Application. Обробник у черзі через хвилину бачить стан, який уже змінився, а кожне нове поле агрегату змінює формат події.
Натомість: кладіть у подію ID і дані про сам факт: хто, коли, з якого етапу.
Чи потрібна вам доменна подія
- Доменна подія
- Незмінний обʼєкт про те, що сталося в домені, з назвою в минулому часі: ApplicationWithdrawn.
- Обробник
- Код, який реагує на подію: шле лист, оновлює лічильник. Агрегат про нього не знає.
- Публікація
- Передача записаних подій обробникам. Відбувається після того, як агрегат збережено.
- Інтеграційна подія
- Подія для інших контекстів чи сервісів. Її формат є публічним контрактом і змінюється обережно, з версіями.
- Outbox
- Таблиця, куди подію пишуть у тій самій транзакції, що й агрегат. Окремий процес читає її й доставляє події, тож жодна не губиться.
Питання зі співбесід
Подія моделі каже, що в базі зʼявився рядок, і спрацьовує на будь-який create(). Доменна подія каже про бізнес-факт мовою домену: ApplicationWithdrawn виникає лише тоді, коли кандидат справді відкликав відгук.
Агрегат лише записує події у свій список. Публікує сценарій або інфраструктура після збереження, бо тільки там відомо, що транзакція вдалася.
Transactional outbox: подію пишуть у таблицю outbox у тій самій транзакції, що й агрегат. Окремий процес читає таблицю й доставляє події, повторюючи до успіху. Обробники мають бути ідемпотентними, бо подія може прийти двічі.