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:
There is one aggregate to represent the transfer
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.
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).