2
votes

I have two "big" entites or aggregates, which have their own business logic - they are saved, updated and destroyed in separate transactions. They have their own child entities, which are manipulated through these aggregate roots. But the problem is, these two aggregates must be in many-to-many relationship to each other. From user interface point of view, there is a kind of UI, where one already existing instance of the second aggregate is added to the first aggregate. In terms of database, there is a table which holds foreign keys to tables of the first and the second aggregate

entity_one_id | entity_two_id
1             | 2
1             | 3
1             | 4

In the example above, an instance of first aggregate holds references to the second aggregate.

And my question is, is it ok from Domain Driven Design perspective, if when saving the first aggregate I load an instance of the second aggregate and add it to the first aggregate. In pseudo-code it may look like:

aggregateOne = aggregateOneRepository->getById(1);
....
aggregate2 = aggregateTwoRepository->getById(2);
aggregate3 = aggregateTwoRepository->getById(3);
aggregate4 = aggregateTwoRepository->getById(4);
aggregateOne->addChildAggregate(aggregate2);
aggregateOne->addChildAggregate(aggregate3);
aggregateOne->addChildAggregate(aggregate4);
aggregateOneRepository->update(aggregateOne);

It seems like in this transaction I do not change the second aggregate and change just one single aggregate. But I'm not sure if DDD theory allows to load multiple different aggregates, when saving one aggregate. So, does this kind of code break the theory or not?

3

3 Answers

4
votes

An aggregate root should not contain instances of other aggregate roots. An aggregate may be passed a transient reference to another when, for instance, invoking a method but it does not hold onto that reference. It is only used in the call.

Your example is actually more common than you may realise. If we had to change to Order and Product aggregates we have a many-to-many relationship. An OrderItem represents that relationship and is best defined as a value object.

When you find that you need to "reference" another aggregate then rather use either only the id or some value object that contains at a minimum the other aggregate's id.

I have a slightly different view on transactions. An aggregate root is a consistency boundary and, as such, fits quite well into a transaction boundary. Every attempt should be made to keep to a single aggregate within a transaction but you also need to be pragmatic about it. If you need a high level of consistency and eventual consistency may not be an option then that is a "rule" that I am willing to bend and include more than one aggregate in a transaction. An example may be processing a journal transaction where an amount is transferred from one account within my system to another. When you have different systems then eventual consistency will have to do and "rolling" back would require compensating operations.

3
votes

Holding reference to another aggregate (be it many-to-many or something else) and updating it in the same transaction, in fact, violates the fundamental principle of aggregate design. An aggregate is a unit of consistency, conforming to its own consistency boundary. A transaction is supposed to update and thereby ensure consistency of only one aggregate.

Updates across aggregates, across consistency boundaries, is naturally necessary. The DDD recommended way for those kind of updates is eventual consistency: updating them later asynchronously in a different transaction. An aggregate refers another aggregate by holding its identifier, rather than a field with relationship (many-to-many in your case). Whenever updating the other aggregate is necessary, leave a domain event containing the other aggregate id published before committing your current transaction. A domain event subscriber picks up the event asynchronously, retrieves the aggregate with the id, makes necessary update, and stores it. That is roughly the basic idea.

3
votes

First of all I agree with Eben, don't have object references of one aggregate within another, use a value object just holding the other aggregates id instead. And in the database this id is simply a string or integer (or whatever you are using as id type in the database) instead of a foreign key.

And always ask yourself what data of the other aggregate do you really need and for what operations of your new aggregate do you need what kind of data at all?

In most cases it turns out simply passing the required data gathered from the first aggregate to a method called on the new aggregate is enough.

If that even happens in the same bounded context I tend to be pragmatic about that. I collect the aggregate I need the data from via it's repository and then pass it as a parameter to the new aggregate's method. Or only some part of it. I usually do this inside an application service.

That way you do not need hold any other information of the old aggregate in the new one rather than it's id but you always have an up-to-date state of the old aggregate wherever you need it. This concept is not even related to domain-driven design but best practice in general, only use the dependency where you really need it.

And if you don't want to rely on the old aggregate's structure simply create some kind of new value object that you populate with the old aggregate's data in the application layer. Therefore you do not even need to gather the data from the old aggregate's repository but simple have some service which only reads the required data from the storage directly. But I would only recommend this if performance is your issue here...

And just one last comment about using foreign keys in databases in monolithic applications:

Don't use foreign keys if you reference something from another bounded context if you ever plan to split up the mononlith at some point. Use logical references instead which you treat as some kind of remote id and resolve them at the application layer. Otherwise separating the database for different services you like to extract from the mononlith can become a nightmare.