Я пытаюсь изучить способы DDD и смежных предметов. Мне пришла в голову идея простого ограниченного контекста для реализации «банка»: есть счета, деньги можно вкладывать, снимать и переводить между ними. Также важно вести историю изменений.
Я идентифицировал сущность Аккаунта, и было бы полезно отслеживать события в нем. Другие сущности или объекты-значения не имеют отношения к проблеме, поэтому я не буду их упоминать.
При рассмотрении депозитов и выводов - это относительно просто, потому что изменен только один агрегат.
При переводе это отличается - два агрегата должны быть изменены одним событием MoneyTransferred . DDD не поддерживает изменение нескольких агрегатов в одной транзакции. С другой стороны, правило источника событий заключается в применении событий к сущностям и изменении их состояния. Если бы событие могло быть просто сохранено в базе данных, проблем не было бы. Но чтобы не допустить одновременного изменения объектов, полученных из событий, мы должны реализовать что-то для управления версиями потока событий каждого агрегата (чтобы сохранить границы транзакций). С версионностью возникает другая проблема - я не могу использовать простые структуры для хранения событий и их чтения, чтобы применить их к агрегированию.
Мой вопрос - как я могу объединить эти три принципа: «одна совокупность - одна транзакция», «событие -> изменение в совокупности» и «предотвращение одновременных изменений»?
источник
Важная деталь в понимании учетных записей на основе транзакций:
balance
атрибутaccount
фактически является примером денормализации. Это для удобства. На самом деле баланс счета - это сумма транзакций, и вам не нужен сам счет для баланса.Принимая это во внимание, акт перевода денег должен состоять не в обновлении,
account
а в вставкеtransaction
.При этом есть еще одно важное правило: акт добавления
transaction
должен быть атомарным с обновлением поля (денормализованного баланса)account
.Теперь, если я понимаю концепцию агрегатов DDD, мне кажется важным следующее :
Так что с точки зрения дизайна DDD я бы предложил:
Существует один агрегат для представления передачи
Агрегат состоит из следующих объектов: передача (корневой объект); корневой объект связан с двумя списками транзакций (по одному для каждой учетной записи); и каждый список транзакций связан с одной учетной записью.
Любой доступ к передаче должен обеспечиваться корневым объектом (
transfer
).Если вы пытаетесь реализовать поддержку асинхронной передачи, тогда ваш основной код должен просто беспокоиться о создании передачи в состоянии «ожидания». У вас может быть другой поток или задание, которое фактически перемещает деньги (вставляет в историю транзакций и, следовательно, обновляет сальдо) и устанавливает перевод в «проведено».
Если вы хотите реализовать в реальном времени блокирующую транзакцию передачи, тогда бизнес-логика должна создать объект,
transfer
и этот объект будет координировать другие действия в режиме реального времени.С точки зрения предотвращения проблем с параллелизмом, первым делом следует вставить дебетовую транзакцию в список транзакций для исходной учетной записи (конечно, обновление баланса). Это должно быть выполнено атомарно на уровне базы данных (с помощью хранимой процедуры). После того, как дебет произошел, остальная часть передачи должна быть в состоянии завершиться независимо от проблем параллелизма, поскольку не должно быть никакого бизнес-правила, препятствующего зачислению на целевой счет.
(В реальном мире банковские счета имеют концепцию заметки, которая поддерживает концепцию ленивого двухфазного коммита. Создание заметки легко и просто, а также ее можно откатить без проблем. Конвертация Заметка для жесткого сообщения - это когда деньги действительно движутся - их нельзя откатить - и представляет собой вторую фазу двухфазного коммита, происходящую только после проверки всех правил валидации).
источник
Я также в настоящее время на стадии обучения. С точки зрения реализации, вот как я чувствую, вы выполните это действие.
Отправьте TransferMoneyCommand, который вызывает следующие события [MoneyTransferEvent, AccountDebitedEvent]
Обратите внимание, что перед тем, как вызвать эти события, необходимо выполнить поверхностную проверку команд и проверку логики домена, т.е. достаточно ли средств на счете?
Сохраняйте события (с контролем версий), чтобы гарантировать отсутствие проблем согласованности. Обратите внимание, что может существовать другая параллельная команда (например, снять все деньги), которой удалось преуспеть и сохранить события до этой, поэтому текущее состояние агрегата может быть устаревшим и, следовательно, события возникают в старом состоянии и являются неправильными. Если сохранение событий не удалось, вам нужно будет повторить команду с самого начала.
Как только события успешно сохранены в базе данных, вы можете опубликовать два события, которые были вызваны.
AccountDebitedEvent удалит деньги со счета плательщика (обновит совокупное состояние и любые связанные модели представления / проекции)
MoneyTransferEvent запускает Saga / Process Manager.
Работа менеджера по саге / процессу будет заключаться в том, чтобы попытаться зачислить средства на счет получателя; в случае неудачи ему необходимо будет вернуть остаток средств плательщику.
Saga / Process manager опубликует команду CreditAccountCommand, которая применяется к учетной записи получателя, и в случае успеха поднимается значение AccountCreditedEvent.
С точки зрения источника событий, если вы хотите отменить это действие, все события в этой транзакции будут иметь идентификатор корреляции / причинности в качестве исходного TransferMoneyCommand, который вы можете использовать для вызова событий для операций отмены / отмены.
Не стесняйтесь предлагать какие-либо проблемы или потенциальные улучшения по вышеупомянутому.
источник