1
votes

Based on my readings, the recommendation in DDD is that an aggregate root should not hold references to another aggregate root. It's preferred to just hold a reference to the ID. I'm trying to figure out if my case would warrant breaking the rule or not.

Using an accounting system as an example, say you have an invoice as an aggregate root. That invoice has lines and payments, those can be entities under the root. But then an invoice also is assigned to a buyer and a supplier. The line items are also linked to accounts, which can have budgets. Buyer, supplier, budget, those are all entities in their own right, that need to be managed and have their own set of rules. But they also affect the business rules in processing invoices. Sure, I can create a domain service and use it to load things separately, but doesn't that just make my domain object less performant and more anemic?

If I can hold a reference to the entity, I can run a single query and just grab all the data I need (I'd be using Entity Framework Core in .NET). If I go with holding a reference to the foreign key, I then need to run a call for each one of the other aggregates I need. And then my domain object can no longer be as rich as it was on its own because it can't process all the business rules it needs without some outside orchestrator (the domain service).

Another thing I wonder is given that these items are NOT going to be modified by the aggregate root at all and are essentially read-only in that context, does that mean I could have them in the same aggregate with a more limited model (the strict minimum), and then they would also have their own aggregate root (so for example I'd have two Budget entities, one under the Invoice root and one under the Budget root). My thinking is that DDD does not really concern itself with the underlying storage, so seems like that could be a valid a option. The entities for a buyer/supplier/budget would also likely be greatly simplified if they were an entity under the invoice aggregate root, whereas the aggregate root version of them would be much more complex, have more properties, business logic, etc.

3

3 Answers

1
votes

the recommendation in DDD is that an aggregate root should not hold references to another aggregate root. It's preferred to just hold a reference to the ID. I'm trying to figure out if my case would warrant breaking the rule or not.

No. The point of an aggregate is to encapsulate business logic and the data it needs to fulfill this business logic in a "transactional" way. You cannot allow that part of an aggregate is modified outside of the control of the root. If your Aggregate A needs the Aggregate B to do its work, you could say that Aggregate B is part of Aggregate A. Given that Aggregate B can change on its own (that's why it's aggregate), that would mean that part of Aggregate A is changing outside of the control of its root. Sorry, that was a convoluted sentence...

There are multiple reasons why you might find yourself with aggregates that need more than the Id of another aggregate:

One possibility is that you don't need them at all. Why does your invoice need more than the Buyer Id and the Supplier Id. What business logic is Invoice doing that needs the details of the Buyer and Supplier? If there is none and the only reason is to be able to display the buyer and supplier information (or to print a PDF), then that's not the aggregate's concern. Two solutions for this are UI composition and a persisted read model.

Another possibility is that you actually need some data produced by another aggregate, but not the aggregate itself. For example, your Invoice will need the product prices to calculate the total amount, but it doesn't need the Product name, description and pictures. It doesn't need either the current price of the product. It needs the price at the moment of purchase. For these scenarios, you can pass a DTO to a method of your aggregate, for example, AddInvoiceLine will expect an InvoiceLineDto with the properties that Invoice needs. A Use Case or Application Service operation will produce this DTO getting the data from one or multiple places (eg partially from user input, partially from a data provider).

So, I think you are in the right direction with your last paragraph, but you first need to figure out, why you are in this position in the first place.

0
votes

I think the best option might be to do it through services. Invoices needs the part of the logic from other domains not all of it. And if those entities/aggregate root are not going to be modified you need only need result of some process. Moreover you can use there hexagonal architecture and abstract domains from the way they are communicating with each other. Communication can occur through http, queues aswell as by references in code. By adding aggregate root to other aggregate you are stick to the last one.

0
votes

Another thing I wonder is given that these items are NOT going to be modified by the aggregate root at all and are essentially read-only in that context, does that mean I could have them in the same aggregate with a more limited model (the strict minimum), and then they would also have their own aggregate root (so for example I'd have two Budget entities, one under the Invoice root and one under the Budget root).

One paper you should review is Pat Helland's Data on the Outside vs Data on the Inside.

The important question is not whether the data is going to be modified, but rather whether or not the data needs to be locked against modification while the primary aggregate is running.

If the data doesn't need to be locked against modification, then you can treat it as data on the outside, which is to say you pass to your aggregate a unlocked copy of the data you need. Roughly translated, that means that your application code fetches a copy of the data and then passes it as an argument to the aggregate.

IF the data does need to be locked against modification, then you've really got data on the inside. So now you need to figure out how that locking mechanism is going to work:

If your domain model is run in a single thread of execution, then you may be fine: the fact that the thread can only do one thing at a time means that you effectively have a lock on "everything".

If all of the data is stored in the same RDBMS, then you may be able to "lock for read" the data as part of your transaction, such that the transaction itself will fail if there is a concurrent modification.

In recent years, where we've been trying to work in an environment with multiple threads of execution and multiple data stores, neither of those constraints hold. So instead, folks have tended to fall back to redesigning their aggregates to support that case: if value A must be locked against modification when we are changing value B, then A and B must be part of the same aggregate (and protected by the same aggregate root).

"Must" here is pretty poorly defined. Another framing that may work better is to think about "What is the business impact of having a failure?" (Greg Young, 2010). If the cost of failure is low, then we can use an unlocked copy of the data, rather than trying to consolidate the data into a single aggregate.