10

I am starting out with DDD and understand that aggregate roots are used to ensure transnational consistency. We should not modify multiple aggregates in one application service.

I would like to know however how to deal with the following situation.

I have an aggregate root called Products.

There is also an aggregate root called Group.

Both have Id's, and can be edited independently.

Multiple Products can point to the same Group.

I have an application service that can change a product's group:

ProductService.ChangeProductGroup(string productId, string groupId)

  1. Check group exists
  2. Get product from repository
  3. Set its group
  4. Write the product back to repository

I also have an application service where the group can be deleted:

GroupService.DeleteGroup(string groupId) 1. Get products from repository whose groupId is set to provided groupId, ensure count is 0 or abort 2. Delete group from groups repository 3. Save changes

My question is the following scenario, what would happen if:

In the ProductService.ChangeProductGroup, we check the group exists (it does), then just after this check a separate user deletes the productGroup (via the other GroupService.DeleteGroup). In this case we set a reference to a product which has just been deleted?

Is this a flaw in my design in that i should use a different domain design (adding additional elements if necessary), or would i have to use transactions?

4 Answers 4

2

We have the same issue.

And I se no way to solve this problem but with transactions or with consistency check at the DB to get an exception in the worst case.

You can also use pessimistic lock, blocking aggregate root or only its parts for other clients until business transaction is completed what is to some extent equivalent to serializable transaction.

The way you go heavily depends on your system and business logic.
Concurrency is not an easy task at all.
Even if you can detect a problem how will you resolve it? Just cancel the operation or allow user to 'merge' changes?

We use Entity Framework and EF6 uses read_commited_snapshot by default, so two consecutive reads from repository can give us inconsistent data. We just keep that in mind for the future when business processes will be more clearly outlined and we can make infromed decision. And yes we still check consistency at model level as you do. This at least allows to test BL separately from DB.

I also suggest you to think over repository consistency in case you have 'long' business transactions. It is quite tricky as it turned out.

1

I think this is not a Domain-Driven Design specific question, but a resource management one.

Resources simply must be locked as they are acquired.

This is true when the application service knows in advance that the acquired resource (the Group aggregate, in your example) is going to be modified. In Domain-Driven Design, resource locking is an aspect that belongs to the repository.

2
  • No, locking is not a "must". In fact it's a pretty horrible thing to do with long-running transactions. You're better off using optimistic concurrency, which every modern ORM supports out of the box. And the great thing about optimistic concurrency is that it also works with non-transactional databases, as long as they support atomic updates.
    – Aaronaught
    Jan 30, 2014 at 3:39
  • 2
    I agree on the drawbacks of locking inside long-running transactions, but the scenario described by @g18c does hint for quite the opposite, that is, brief operations over individual aggregates.
    – rucamzu
    Jan 30, 2014 at 8:48
0

Why don't you store the product ids in the Group entity? This way you are only dealing with a single aggregate which will make things easier.

You will then need to implement some type of concurrency pattern e.g. If you choose optimistic concurrency simply add a version property to the Group entity and throw an exception if the versions don't match when updating i.e.

Add product to a group

  1. Check if product exists
  2. Get Group from repository (including its version id property)
  3. Group.Add(ProductId)
  4. Save Group using repository (update with new version id and add a where clause to ensure version has not changed.)

Lots of ORM's have optimistic concurrency built in using version ids or timestamps but it is easy to roll your own. Here is a good post on how its done in Nhibernate http://ayende.com/blog/3946/nhibernate-mapping-concurrency .

0

Although transactions might help, there is another way, especially if your scenario is limited to just a few cases.

You could include checks in the queries that do the mutations, effectively making each check-and-mutate pair an atomic operation.

The product-updating query inner joins the (newly referenced) group. This causes it to update nothing if that group is gone.

The group-deleting query joins any products pointing to it and has the additional condition that (any one column of) the join result must be null. This causes it to delete nothing if any products point to it.

The queries return the number of rows that matched or were updated. If the result is 0, then you lost a race condition and did nothing. It can throw an exception, and the application layer can handle that however it wants, such as by trying again from the start.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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