Geeks With Blogs


Dylan Smith ALM / Architecture / TFS

In the last couple of posts I talked about how larger aggregates make enforcing invariants easier, but smaller aggregates reduce concurrency conflicts.  You need to use domain knowledge to choose aggregate boundaries that minimize the chances of invariants spanning aggregates, and minimize the chances that multiple users will be editing the same aggregate simultaneously.


In this post I want to cover how I enforce the invariants (hopefully few) that do need to span aggregate boundaries.  As I see it there are basically two choices:

  1. Multi-Aggregate Locking
  2. Minimize-And-React Approach

If you recall the problem with enforcing these invariants is that you need to acquire a lock on multiple aggregates in order to prevent a race condition.  Lets look at the example of preventing customer orders that exceed that customers credit limit.  In order to enforce that you’d have some code that checked the invariant when creating new orders.  It would essentially do something like:

if (SUM(All-Outstanding-Orders) + NewOrder.Total > Customer.Credit_Limit)

then Reject-Order

else Save-Order


In order to avoid race conditions we need to guarantee that none of the data in the if statement changes between the time that the condition is evaluated and the order is saved.  In this case that means none of the existing Outstanding orders can change, and the customers credit limit can’t change, also no new outstanding orders can be created.  If we assume that Customer and Order are separate aggregates, that means that we need to lock the Customer aggregate, and each aggregate corresponding to an Outstanding Order.  The tricky part can be that we also need to ensure no new Order Aggregates for that customer are created.

Most people I talk to about this are surprised at the complexity I seem to be talking about.  They think that they have been writing applications for many years and never had to worry about this consistency stuff.  Indeed, I used to think the same way.  However, it turns out most applications have subtle race conditions such as the above and nobody even realizes it or cares.  And this is perfectly acceptable!  Rare race conditions, may not be worth the development effort to eliminate and/or handle.  However, as an application architect at a minimum I like to know that these race conditions exist and can make a conscious decision whether I want to invest time dealing with them or not.  Even if the decision is that it’s not worth worrying about, I want to make sure that these are explicit decisions, rather than unexpected surprises when they may arise later.


So having said that, we know we have an invariant that crosses aggregate boundaries, so there is a race condition.  What are our options?  Of course we can just ignore it. A race condition will rarely occur, and the impact of it occuring (e.g. a customer getting an order approved that may exceed their credit limit) may be acceptable.  Especially in applications that measure the users in dozens, race conditions should be extremely rare.  However, if you measure your users in thousands (or more) the rare race condition, may actually occur quite frequently.


The first option from above is to implement some form of multi-aggregate locking.  For the Customer Credit Limit example, we would need a way to lock the customer aggregate (which contains the credit limit data), all active orders, and also lock all new orders for that customer.  There are a few options for doing this at the application level, and even at that database level (perhaps using transaction isolation levels).  However, I typically try and avoid the complexity involved in this approach.


The 2nd option is what I’m calling “Minimize-and-React”.  Rather than trying to eliminate the race condition (via locking), try to minimize it (by doing the check at the last possible moment – as we probably are already doing), then put in place a mechanism to detect when the race condition has been violated and react appropriately.  In a lot of cases the “react” portion should probably just be sending an email to a human to investigate.  When using an architecture that uses “Domain Events” you can create what some people call a “Saga” (although not an entirely accurate term).  In this case you would create a Saga for each cross-aggregate invariant you wish to enforce, and have it subscribe to the appropriate events to detect when the invariant has been violated.  Then take the appropriate actions (e.g. send an email to notify somebody, or possibly execute compensating actions).


In the Customer Credit Limit example, I could create a Saga that subscribed to the events: CustomerCreditLimitChanged, OrderCreated (and probably other events such as CustomerOrderChanged, OrderCancelled, etc).  Basically, any events which could impact the evaluation of the invariant.  Since the Sage subscribes to Events, which by definition represent actions which have already occurred, it can detect violations of the race conditions without the race conditions present in the Domain Model (Aggregates).  So in the saga I would subscribe to the various events, and in each handler call the code to check whether the invariant has been violated.  Then take the appropriate action in response – typically either sending a notification to somebody, or taking some correcting/compensating action.

Posted on Sunday, May 5, 2013 4:38 PM | Back to top

Comments on this post: Choosing Aggregate Boundaries – Invariants Spanning Aggregates

# re: Choosing Aggregate Boundaries – Invariants Spanning Aggregates
Requesting Gravatar...
Great articles!
Left by inf3rno on Oct 17, 2017 9:55 AM

Your comment:
 (will show your gravatar)

Copyright © Dylan Smith | Powered by: