DDD: creating multiple aggregates with a shared life-cycle in a single transaction
Asked Answered
W

3

8

I'm aware of the general rule that only a single aggregate should be modified per transaction, mostly for concurrency and transactional consistency issues, as far as I'm aware.

I have a use case where I want to create multiple aggregates in a single transaction: a RestaurantManager, a Restaurant, and a Menu. They seem like a single aggregate because their life-cycles begin and end together: it doesn't make sense within the domain to create a RestaurantManager without a Restaurant, or vice versa; the same goes for a Restaurant and a Menu. Further, if the Restaurant or the RestaurantManager is deleted (unregistered), they should all be deleted together.

However, I've split them into separate aggregates because, once created, they are updated separately, maintain their own invariants, and I don't want to load them all into memory just to update one property on the Restaurant, for example.

The only thing that ties them together is their life-cycle.

My question is whether this represents a case where it is okay to go against the "rule" that each transaction should only operate on a single aggregate.

I'd also like to know if I should enforce their shared life-cycle in the domain model by having each aggregate root hold the identifier of the aggregate root it depends on, i.e. by having Restaurant require a MenuId as a constructor parameter, and likewise for Menu and RestaurantId, so that neither can be created without the other. However, this still wouldn't enforce that they should be saved together by the application service anyway, since it could create them all in memory, then only save the Menu, for example.

Waligore answered 8/10, 2020 at 11:45 Comment(2)
I don't know if you mean "delete" in the real sense, so this is for your consideration: udidahan.com/2009/09/01/dont-delete-just-dont – Swollen
Had time to review my answer yet? – Mcclean
W
1

As pointed out by @plalx, contention doesn't matter as much when creating aggregates in terms of transactions, since they don't yet exist so can't be involved in contention.

As for enforcing the mutual life cycle of multiple aggregates in the domain, I've come to think that this is the responsibility of the application layer (i.e. an application service, or use case).

Maybe my thinking is closer to Clean or Hexagonal architecture, but I don't think it's possible or even sensible to try and push every single business rule down into the "domain model". The point of the domain model for me is to partition the problem domain into small chunks (aggregates), which encapsulate common business data/operations that change together, but it's the application layer's responsibility to use these aggregates properly in order to achieve the business' end goal (which is the application as a whole), including mediating operations between the aggregates and controlling their life cycles.

As such, I think this stuff belongs in an application service. That being said, frequently updating multiple aggregates in each use case could be a sign of incorrect domain boundaries.

Waligore answered 11/10, 2020 at 19:19 Comment(0)
S
7

Your requirement is a pretty normal use case in DDD, IMHO. There are always multiple aggregates working in tandem to support the application, and they are interlinked in their lifecycles. But the modeling concepts still stand true. Let me attempt to explain what your model would look like with the help of a few DDD rules:

Aggregates are transaction boundaries

Aggregates ensure that no business invariants are broken at any point. This means that if you have multiple aggregates strung together as part of one transaction, you have to load all of them into memory for the validation.

This is especially a problem when your application is data-rich and stores data in a database cluster - partitioned, distributed (think Mongo or Elasticsearch). You will have the problem of loaded up data from potentially different clusters as part of a single transaction.

Aggregates are loaded in entirety

Aggregates and their associated data objects are loaded in entirety into memory. This means that unnecessary objects (say the restaurant's schedule for the upcoming month, for example) for the transaction may be loaded into memory. By itself, this is not a problem. But when multiple aggregates get together, the amount of data loaded into memory needs to be considered.

Aggregates refer to each other by their unique identifiers

This one is straightforward and means that each aggregate stores its referenced aggregates by their identifiers instead of enclosing the other aggregate's data within it.

State changes across Aggregates are handled through Domain Events

In cases where you want a state change in one aggregate to have side-effects on other aggregates, you publish a domain event, and a subscriber handles the change on other aggregates in the background. This is how you would want to handle your requirement for cascade deletes.


By following these rules, you are essentially zooming in one single aggregate at a time and ensuring that the complexity remains low. When you string up multiple aggregates, though it is clear and understandable on day 1, eventually, the application tends towards becoming a big ball of mud, as dependencies and invariants start crisscrossing each other.

Swollen answered 8/10, 2020 at 15:32 Comment(3)
When I dont want to deal with eventual consistency I often handle domain events synchronously to participate in the same transaction while keeping modules decoupled. This has the advantage of remaining simple while allowing to switch to eventual consistency easily later. What do you think of this for non-distributed systems? – Mcclean
Completely fine πŸ‘ I frequently use two message brokers in my applications: one in-memory and one distributed (RabbitMQ, ZeroMQ, Kafka). The in-memory would be injected based on the test configuration, and the distributed broker would be injected in production. We also often use the in-memory broker when beginning development. The only thing one needs to remember is when we switch to eventual consistency, we may have to add missing "corrective policies" because the earlier transaction would have gone through. – Swollen
Yes, compensating actions & transitional states must be added. Queries may need some love too ;) – Mcclean
M
3

"only a single aggregate should be modified per transaction"

Contention at creation doesn't matter as much. You can create many ARs in a single transaction without problem because the only other operation that could conflict is another duplicate creation process.

Another reason to avoid involving many ARs in a single transaction is coupling between modules though, but you could always keep things loosely coupled using synchronously dispatched domain events.

As for the deletion, it's probably less problematic to make it eventually consistent. Does it really matter that Restaurant is closed while RestaurantManager remains registered for a short period of time?

The fact you are asking this question tells me your system is not distributed? If your system is running with a single DB server and used by a few people it may be that eventual consistency make things more complex for scalability you don't actually need.

Start simple and refactor as needed, but crossing AR boundaries is not something that should be done consistently or else your boundaries are clearly wrong.

Furthermore, if you want to communicate that a RestaurantManager can't be spawned from nowhere and associated with an invalid RestaurantId by mistake you may want to look at your ubiquitous language for guidance.

e.g.

"A RestaurantManager is registered for a given Restaurant": not sure it truly aligns with your UL, but it's just for the sake of the example.

RestaurantManager manager = restaurant.registerManager(...);

This obviously increases coupling and could affect performance, but it aligns well with the UL and makes it more difficult to misuse the model. Also note that with a single DB, you could enforce referential integrity which takes cares of these uninteresting referential constraints.

Mcclean answered 8/10, 2020 at 15:13 Comment(5)
Yes that makes sense about contention at creation -- I was thinking the same thing. As far as domain events go, my system isn't distributed like you guessed, so I don't feel the need to introduce that complexity here. Anyway, I've come to think that this kind of thing belongs in the application layer. Maybe my thinking is more aligned with Clean/Hexagonal architecture, but I don't think it's possible or even sensible to push every single piece of business logic down into the "domain model"... – Waligore
I think the point of aggregates is to break down the problem domain into small chunks that encapsulate data/operations that change together, but it's the application as a whole that's important, and the application layer's responsibilities involve overseeing the life cycle of aggregates, and mediating between them where appropriate. You're probably right that crossing AR boundaries too often is a smell, but I think it makes sense in this particular case. – Waligore
@Walker Could you answer your own question so that others see how you solved the problem? – Mcclean
Yes I will do now. What do you think of what I said though, do you agree/disagree? – Waligore
@JordanWalker I don't agree/disagree as I don't have enough knowledge of the specific solution you chose to go with, which is also why I'm waiting to read your answer ;) Domain events aren't just useful in distributed systems though. I agree that sometimes domain rules (especially uninteresting ones) are better handled outside the domain itself (e.g. foreing keys, unique constraints, etc) when possible. Expressing the UL in code is also very important in DDD though. For that reason I like the restaurant.registerManager(...) solution here. The referential check is free and it expresses the UL – Mcclean
W
1

As pointed out by @plalx, contention doesn't matter as much when creating aggregates in terms of transactions, since they don't yet exist so can't be involved in contention.

As for enforcing the mutual life cycle of multiple aggregates in the domain, I've come to think that this is the responsibility of the application layer (i.e. an application service, or use case).

Maybe my thinking is closer to Clean or Hexagonal architecture, but I don't think it's possible or even sensible to try and push every single business rule down into the "domain model". The point of the domain model for me is to partition the problem domain into small chunks (aggregates), which encapsulate common business data/operations that change together, but it's the application layer's responsibility to use these aggregates properly in order to achieve the business' end goal (which is the application as a whole), including mediating operations between the aggregates and controlling their life cycles.

As such, I think this stuff belongs in an application service. That being said, frequently updating multiple aggregates in each use case could be a sign of incorrect domain boundaries.

Waligore answered 11/10, 2020 at 19:19 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.