Comments 15
Насколько я понял, получается, что мы сначала отправляем событие, получаем OK, что оно опубликовано, а уже потом сохраняем в БД запись, так?
А если сохранение в БД отвалится, а событие уже улетело? По-моему, более правильным подходом является отправка событий через @TransactionalEventListener(BEFORE_COMMIT)
и если отправка не удалась, то и транзакцию ещё не поздно зафейлить.
Не так, агрегат содержит в себе событие и при сохранении агрегата сохраняется и он сам и событие. И все это происходит в рамках одной транзакции, атомарно. А затем уже выполняется публикация.
Что касается варианта с TransactionalEventListener(BEFORE_COMMIT)
то может быть ситуация, когда уже событие опубликовалось, а транзакция по какой-либо причине завершилась с ошибкой, что привело к ее откату и опять же может возникнуть несогласованное состояние. Плюс сам брокер может быть какое-то время недоступен, получается из-за его недоступности будет блокироваться и выполнение самой транзакции. А если публикация будет асинхронной опять же нет достоверной уверенности а прошла ли она корректно.
Ваш вариант с TransactionalEventListener
можно задействовать, чтобы не публиковать событие, а выполнять их сохранение с Transactional(propagation = Propagation.SUPPORTS)
тогда и сущность и событие сохранятся в рамках одной транзакции.
К слову, есть другая интересная аннотация DomainEvents
, которая фактически автоматом публикует доменные события, она может быть использована вместе с Transactional
для метода save у репозитория, при сохранение самой сущности и с EventListener
и Propagation.SUPPORTS
для сохранения в рамках одной транзакции самого события. Такое решение будет максимально близким к тому, о чем говорилось в статье.
Если бизнес-данные и события хранятся в разных базах данных и их нельзя поместить в одну транзакцию - как в этом случае построить алгоритм публикации события?
При каких обстоятельствах может возникнуть такая ситуация? Агрегатом владеет какой-то микросервис, у этого микросервиса имеется база данных. И весь этот алгоритм применяется в рамках этой базы данных. Сохраняется агрегат и событие в одну и ту же базу данных.
Абсолютно стандартная ситуация - продуктовая бд на оракле, а бд на которой можно реализовать Transactional outbox pattern на постгре.
Гарантированно атомарный - никак.
С костылями и приседаниями
Обмазывать сущности состоянием и, сохраняя бизнес-сущность, проставлять им статус "сохранено без события". При сохранении события ставить ей статус "сохранено", а у сущности менять статус на "сохранено с событием". После успешной публикации, менять статус события на "опубликовано".
Плюс должен вертеться фоновый процесс, следящий за соответствием статусов сущностей и событий, и как-то (тут зависит от конкретного случая) реагировать на такие ситуации.
А в качестве гарантий атомарности, чем не устроили транзакции?)
Ваша схема напоминает шаблон Сага для распределенных систем.
А в данном примере вполне может быть использована локальная транзация и для сохранения сущности и для для сохранения события
напоминает шаблон Сага
Не напоминает, а она и есть. :)
может быть использована локальная транзация
А какой в этом смысл? Как транзакции между двумя БД синхронизовать будете?
Идея была все же не в разных базах и синхронизации между ними транзакций, а в использование одной базы и транзакции, для сохранения агрегата и события. Заботы о распределенных транзакциях в данной статье нет. Transactional outbox pattern решает проблему согласованного состояния базы данных и публикации доменного события с гарантией минимум один раз.
Сохранение в бд и агрегата, и события в принципе понятно.
А тема вычитки из бд и отправки в очередь, например, в Kafka, раскрыта не полностью.
Как при отправке в Kafka гарантировать, что события по одному субъекту отправляются в том же порядке, что и были сохранены? И желательно не в один поток
Автор молодец. Ту же самую связку реализовал на C# через EF Core + библиотека SAP как функционал по сохранению и публикации событий. Всё также делается атомарно в рамках одной транзакции и сразу же публикуется после коммита. После публикации статус события меняется.
Реализация Transactional outbox pattern и немного DDD