Search code examples
domain-driven-designaggregateroot

Breaking DDD aggregate root reference rule


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.


Solution

  • 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.