Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle (partially) dependant aggregate roots?

I have domain concept of Product.

Product have some GeneralDetails, lets say: sku, name, description. At the same time, Product have some ProductCalculations part where accountants can put different values like purchasePrice, stockLevelExpenses, wholeSalesPrice, retailPrice.

So, so far, Product would look something like:

class Product{
   GeneralDetails Details;
   ProductCalculations Calculations;

   ChangeDetails(GeneralDetails details){}
   Recalculate(ProductCalculations calculations{}

}

This setup would make Product an aggregate root. But now, i want to split it in a way that Product manager can input/update product details but then that accountant can step in and intependently change calculations for given product without concurrency issues. That would suggest splitting it into 2 separate aggregate roots.

But then, deleting ProductDetails aggregate must mean deleting ProductCalculations too and it should happen in transactional way.

Assuming they are 2 aggregate roots, meaning they have 2 separate repositories with corresponding Delete methods, how to implement this as an atomic transaction?

The only thing i can think about is to raise event when ProductDetails gets deleted, have a handler (DomainService) that uses some special repository that handles transactions over multiple aggregate roots.

Is there some problem with that approach and/or is there some better way to handle it?

PS. I cannot allow eventual consistency when ProductDetails is deleted.

PS2. Based on comments from @Jon, Details and Calculations create&delete should be synced in a way that when Details are created/deleted, Calculations should also be created/deleted. On the other hand, their updates should be completely independent.

like image 562
dee zg Avatar asked Aug 15 '18 14:08

dee zg


People also ask

Can an aggregate root contain another aggregate root?

So in its own bounded context then yah you could say it's and aggregate root. In DDD you can reference an aggregate root from another aggregate. What you cannot do is reference anything inside the other aggregate root.

How do you choose the aggregate root?

When choosing an aggregate root you choose between Transactional Consistency and Eventual Consistency. When your business rules allow you would rather favour Eventual Consistency.

What are aggregate roots?

Aggregate Root is the mothership entity inside the aggregate (in our case Computer ), it is a common practice to have your repository only work with the entities that are Aggregate Roots, and this entity is responsible for initializing the other entities. Consider Aggregate Root as an Entry-Point to an Aggregate.

Can a value object be an aggregate root?

Thus, the aggregate root must be an entity, not a value object, so that it can be persisted to and from a data store using its ID.

What is the difference between aggregate root and data model?

If you’re not exposing data from an aggregate, meaning there’s no way for callers/consumers to get data from it (think CQRS), then you can encapsulate your data model inside your aggregate root. Your data model at that point is your Entity Framework entities. Your aggregate root is your aggregate.

What is an aggregate root in DBMS?

All business operations should go through the root. This way, the aggregate root can take care of keeping the aggregate in a consistent state. The root is what takes cares of all our business invariants. And in our example, the Order class is the right candidate for the aggregate root.

What is an aggregate root in CQRS?

If you are new to CQRS you may want to see where an Aggregate Root fits into a typcial CQRS architecture. You can do that here. The job of an Aggregate Root is to control and encapsulate access to it’s members in such a way as to protect it’s invariants.

When do we need to issue result<T> when mutating an aggregate?

} The requirement now is that every time we mutate an aggregate, that needs to issue a Result<T>. Going back to our User aggregate, we can change the method to accomodate our design decision.


Video Answer


3 Answers

I think the answer to your question depends somewhat on what data storage technology you're using and your data storage model, because if you can push operation transactionality to the data layer, things get much easier.

If you're using a document-oriented database (Cosmos DB, MongoDB, etc...), I would model and store your Product aggregate (including Details and Calculations) as a single document and you get the atomic transaction and concurrency checking for free from the database.

If you must store these as separate documents/records in your data store, then providing atomic transactions and concurrency checking becomes your concern. For years folks (especially those using Entity Framework) have been using the Unit of Work pattern to batch up multiple repository operations and submit them to the database as a single operation (EF-specific UoW implementation). Rob Conery suggests here that a better option is to use Command objects to encapsulate a multi-part operation that needs to be executed as a single transaction.

In any event, I would encourage you to keep the management of this operation within Product, so that consumers of Product are unaware of what's going on during the save - they just blissfully call product.SaveAsync() and they don't need to know whether that's causing one record update or ten. As long as Product is injected with the repositories it needs to get the job done, there's no need to have a separate domain service to coordinate this operation. There's nothing wrong with Product listening for events that its children raise and responding appropriately.

Hope this helps!

like image 186
Jon Avatar answered Oct 17 '22 04:10

Jon


" I cannot allow eventual consistency when ProductDetails is deleted"

Why not? What would be the business cost of having Inventory.Product exist while Finance.Product doesn't or vice-versa?

"but then that accountant can step in and intependently change calculations for given product"

That's pretty much what eventual consistency is, no?

If you really can't have eventual consistency then use a domain service to create/delete two distinct aggregate roots in a single transaction, but ask yourself how you are going to do that if the information is not entirely provided by the same end user?

like image 41
plalx Avatar answered Oct 17 '22 03:10

plalx


I agree with @plalx in almost every point. However I want to do my bit to the discussion.

I've found that there is usually a very little cost in creating two or more related aggregates inside a single transaction (inside a single bounded context). After all, if those aggregates don't exist yet there cannot be a concurrency conflict, there is no contention and no much difference. Furher, you don't need to deal with partially created state (thinking that state is split between aggregates). It is possible to do that using eventual consistency, and there are situations where that is a better approach, but most of the time there is no great benefit. Even Vernon in his book Implementing Domain-Driven Design mentions this use case as "valid reason to break the rules".

Deleting more than one aggregate is a different story. What should happen if you delete and aggregate that another user is updating at the same time? The probability of such a conflict increases as more aggregates you try to modify/delete in the same transaction. Is there always an upstream/downstream relationship between those aggregates? I mean, if an user deletes A and B must be also deleted, have the user that is updating B no "power" or "voice" to cancel that deletion since she is providing more information to the state of the aggregate?

Those are a very tricky questions and most of the time it is something you need to discuss with a domain expert, and there are very few real scenarios when the answer is something you can't afford with eventual consistency. I discovered that in many cases is preferable to put a "flag" marking the aggregate as "inactive", notifying that will be deleted after some period of time. If no user with enough permission request that aggregate to become active again, then it gets deleted. That helped users to not kill themselves when they delete some aggregate by mistake.

You've mentioned that you don't want a user to spend hours modifying one aggregate if there is a deletion, but that is something that a transaction doesn't contribute much. This is very dependent in the whole architecture, though. That user could have loaded the aggregate into her own memory space and then a deletion occurs. It doesn't matter if you delete inside a transaction, the user is still wasting time. A better solution could be to publish a domain event that triggers some sort of push notification to the user, so she knows that a deletion happened and can stop working (or request a cancellation of that deletion, if you follow such approach).

For the reports and calculations, there are many cases when those "scripts" can skip records where the sibling aggregate is gone, so users doesn't notice there is a missing part or there is no complete consistency yet.

If for some reason you still need to delete several aggregates in the same transaction you just start a transaction in an application service and use repositories to perform the deletion, analogous to the creation case.

So, to summarize:

  • The rule of "modify one aggregate per transaction" is not that important when there is a creation of aggregates.
  • Deletion of many aggregates works quite well (most of the time) with eventual consistency, and very often just disabling those aggregates, one at a time, is better than performing the deletion immediately.
  • Preventing an user from wasting time is better achieved with proper notifications than transactions.
  • If there is a real need to perform those actions inside a single transaction, then manage that transaction in the application an be explicit. Using a domain service to perform all the required operations (except for the transaction that is mostly an application concern) brings that logic back to the domain layer.
like image 35
Daniel Avatar answered Oct 17 '22 05:10

Daniel