10

I am trying to learn ways of DDD and related subjects. I came up with an idea of simple bounded context to implement "bank": there are accounts, money can be deposited, withdrawn and transferred between them. It is also important to keep history of changes.

I identified Account entity and that event sourcing would be good to keep track of changes in it. Other entities or value objects are irrelevant to the problem, so I won't mention them.

When considering deposits and withdrawals - it's relatively simple, because there is only one aggregate modified.

When transferring it's different - two aggregates must be modified by one MoneyTransferred event. DDD deprecates modifying multiple aggregates in one transaction. On the other hand event sourcing's rule is to apply events to entities and modify state based on them. If event could be stored simply in database, there would be no problem. But to prevent concurrent modification of event sourced entities we must implement something versioning the event stream of each aggregate (to keep their transaction bounds). With versioning comes another problem - I cannot use simple structures to store events and read them back to apply them to aggregate.

My question is - how can I bring together those three principles: "one aggregate one transaction", "event->change in aggregate" and "concurrent modification prevention"?

7

When transferring it's different - two aggregates must be modified by one MoneyTransferred event.

Transferring money is a separate act from updating the ledgers.

MoneyTransferred
AccountCredited
AccountDebited

The exercise that finally broke this loose for me was realizing that AccountOverdrawn is an event, it describes the state of the account without regard to the other participants in this exchange, so there must be a command run against an account that produces it.

You can't reasonably derive state like AccountOverdrawn from the read model, because you can't possibly know if you have seen all of the events yet -- only the aggregate itself has a full view of the history at any given moment.

The answer, of course, is right there in the ubiquitous language -- accounts are credited or debited to reflect the bank's obligations to its customers.

Allright, but it means I should use AccountCredited and AccountDebited events for deposits and withdrawals as well, so I only register not the cause of change, but the change caused by some other action. If I would like to reverse the action I couldn't, because not all of the events are registered.

I'm not entirely certain that follows, because you do have (for cases like this one) a natural correlation identifier, which is the transaction id itself.

Second thing - it means i need to use something like saga.

Slightly different spelling: you need something like a human being dispatching the right commands.

There are at least two ways you could do it. One would be to have a subscriber listening for MoneyTransferred, and dispatching the two commands to the ledgers.

Another alternative would be to track the processing of the transaction as a separate aggregate -- think of it as a checklist of all the things that need to get done since a transaction occurred. So a MoneyTransferred event handler dispatches ProcessTransaction, which schedules work to be done and checks off what work has been completed.

  • Allright, but it means I should use AccountCredited and AccountDebited events for deposits and withdrawals as well, so I only register not the cause of change, but the change caused by some other action. If I would like to reverse the action I couldn't, because not all of the events are registered. How can I do this(causality of events)? Second thing - it means i need to use something like saga. How should be a transfer modeled then? A the moment i have transfer method on account. When called it publishes event MoneyTransferred. I don't know what should start something such as saga. – cocsackie Feb 11 '17 at 14:32
  • Isn't it -> AccountCredited and AccoundDebited then MoneyTransferred? First solution updates both aggregates in one transaction (no consistency guarantee of any kind)? There is also no aggregate which could publish MoneyTransferred -> no correlation. Second solution seems to be better - ProcessTransaction can publish MoneyTransferred and to avoid multiple aggregate modification in one transaction I can publish events from Account after committing transaction. Sorry for being finicky. It is hard to understand for beginner - cannot use only one pattern w/o other. – cocsackie Feb 12 '17 at 20:19
1

An important detail in understanding transaction-based accounts: the balance attribute of account is actually an instance of denormalization. It's there for convenience. In reality, the balance of an account is the sum of its transactions, and you don't really need the account itself to have a balance.

Bearing this in mind, the act of transferring a money should not be to update account but to insert into transaction.

That being said, there is another important rule: the act of adding a transaction should be atomic with an update to the (denormalized balance field of) account.

Now if I understand the DDD concept of aggregates, the following seems relevant:

The aggregate is a logical boundary for things that can change in a business transaction of a given context. An aggregate can be represented by a single class or by a multitude of classes. If more than one class constitutes to an aggregate then one of them is the so called root class or entity. All access to the aggregate from outside has to happen through the root class.

So in terms of DDD design I would suggest:

  1. There is one aggregate to represent the transfer

  2. The aggregate is composed of the following objects: the transfer (the root object); the root object is linked to two transaction lists (one for each account); and each transaction list is linked to one account.

  3. All access to the transfer should be meditated by the root object (the transfer).

If you are trying to implement asynchronous transfer support, then your main code should just worry about creating the transfer, in a "pending" status. You may have another thread or a job that actually moves the money (inserting into transaction history, and therefore updating balances) and sets the transfer to "posted."

If you are looking to implement a real-time, blocking transfer transaction, then the business logic should create a transfer and that object would coordinate the other activities in real time.

In terms of preventing concurrency issues, the first order of business should be to insert the debit transaction into the transaction list for the source account (updating the balance, of course). This would have to be performed atomically at the database level (via a stored procedure). After the debit has occurred, the rest of the transfer should be able to succeed regardless of concurrency issues, as there shouldn't be any business rule preventing a credit to the target account.

(In the real world, bank accounts have the concept of a memo post which supports a concept of a lazy two-phase commit. Creation of the memo post is lightweight and easy, and also it can be rolled back without issue. Conversion of the memo post to a hard post is when the money actually moves-- this can't be rolled back-- and represents the second phase of the two-phase commit, occuring only after all the validation rules have been checked).

0

I am also currently at the learning stage. From an implementation point of view, this is how I feel you will perform this action.

Dispatch TransferMoneyCommand which Raises following events [MoneyTransferEvent, AccountDebitedEvent]

Note before it raises these events, superficial command validation and domain logic validation will need to be performed, i.e. does the account have enough balance?

Persist the events (with versioning) to ensure there are no consistency issues. Note that there could be another concurrent command (like withdraw all money) which managed to succeed and save events before this one, so the current state of the aggregate may be out of date and therefore the events are raised on old state and are incorrect. If the saving of events fails, you will need to retry the command from the start.

Once the events are successfully saved in the database you can publish the two events which were raised.

AccountDebitedEvent will remove the money from the payor's account (updates the aggregate state and any related view/projection models)

MoneyTransferEvent starts Saga/Process Manager.

Job of the saga/process manager will be to try to credit the account of the payee, if it fails, it will need to credit the balance back to the payor.

Saga/Process manager will publish a CreditAccountCommand which is applied to payee's account and if successful than AccountCreditedEvent will be raised.

From an event sourcing point of view, if you wanted to reverse this action, all the events in this transaction will have the correlation/causation id as the original TransferMoneyCommand which you can use to raise events for undo/reversals operations.

Feel free to suggest any issues or potential improvements on the above.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.