DDD - Enforce invariants with small aggregate roots
Asked Answered
M

2

9

I'm having my first attempt at DDD and I'm running into a problem with aggregate design.

My application contains 3 Entities; Graph, Node, Link. Each of these entities has a name property which can be modified by the user (which I believe makes 'name' unsuitable as an Entity id). A Graph contains a collection of Nodes and a Node has a collection of outgoing Links (for the purpose of this problem it is safe to ignore incoming links). Each Node can only be associated with one Graph at a time (but can be moved between Graphs) and similarly, each Link can only be associated with one Node at any given time (but can be moved).

The invariant(s) I am attempting to enforce is that all Entity names are unique within their parent collection. With the architecture described above, the invariant is on the actual collections, so I decided that the collection owners (Graph and Node) should both be Aggregate Roots.

The problem I have is how do I now enforce the name invariant on Node? On Link it is easy since it is hidden away inside the Node AR and as such Node can confirm that all Link renames/moves do not break this invariant. But as far as I can see, there is nothing to prevent a direct rename of Node which could break the invariant. Eventual consistency is not an acceptable option here, this must be a true system invariant.

The approach I am considering is to have Node.Rename() actually enforce the invariant, but my concern is that this involves looking inside its parent Graph to check if the rename is valid. This doesn't 'feel' right - it feels like the Graph should be the one to enforce this namespacing constraint and that Node should know nothing about it at all.

I hope this makes sense, and I look forward to hearing peoples thoughts.

Edit: The Domain Model presented above is a simplified subset of the entire Domain. Too complex for all entities to be held within a single AR.......

Mombasa answered 31/7, 2013 at 10:27 Comment(5)
"it feels like the Graph should be the one to enforce this namespacing constraint and that Node should know nothing about it at all." - bingo!Proglottis
@Proglottis - So are you suggesting the only AR should be Graph? I don't see how else I can enforce that all Node renames should go via the parent Graph. If this is the case, I worry that I will end up with one giant 'god' AR - similar to the First Attempt in the Vaughan essayMombasa
If it's a true system invariant, keeping Graph as one AR seems the way to go. If you use Graph as the only AR though, how large can it grow, and how much concurrency do you expect?Kinslow
@Kinslow - Concurrency shouldn't be a problem but the Graph as a single AR would more than likely grow HUGE (the problem presented above is a much-simplified subset of the entire domain). I will edit the question and include this info as it is pertinent to the solution.Mombasa
This structural coupling will get you into problems then. Is laying a domain service over the aggregates which enforces cross-aggregate invariants an option?Kinslow
M
6

The solution I found to this problem came from taking a CQRS approach. I found an excellent CQRS introduction here.

In my 'write' model; Graph, Node and Link are all Aggregate Roots, but names are entirely managed by the parent collection. Thus in the write model, a Node has no idea what its own name is (meaning name updates have to go via the owning Graph). In the corresponding 'read' model, the name is associated directly with the Node (since this is useful for display).

The advantage of this solution is that it allows me to keep my ARs small, but since the 'name' info is held inside the parent collection, I have no issues with maintaining cross-aggregate invariants.

Mombasa answered 6/8, 2013 at 15:9 Comment(2)
Good to hear you were able to solve your problem. I've read about CQRS as well, but never really put it into practice. But maybe I should, it would sure make parts of my projects easier to implement :)Aligarh
Do you have a graph.renameNode(node, name) or a node.renameChild(childNode, name) method? What is the exact implementation by getting and setting the node names? Where do you store the names? Are they node attributes or are there in collections, e.g. name -> node maps?Asiatic
A
21

As you already concluded in your comment, the only aggregate root should be Graph.

There is a difference between aggregates and aggregate roots. In your example both Graph and Node are aggregates, but the object responsible for managing the entire aggregate is Graph. So this is the root.

The easiest way to get an idea if an object is an aggregate root is to ask yourself:

Does it make sense to have just this object, detached from its parent?

If the answers is no, then it's probably not an aggregate root. For example, a single Node is probably of little use when it's not part of a parent Graph. That's why you usually only have repositories for aggregate roots; to prevent yourself from having access to objects that are not part of their corresponding aggregate root.

Now on to the invariants. You stated that (emphasis mine):

all [Node] names are unique within their parent [Graph]

You basically answered your question right there. In the context of a single Node it doesn't make sense to say that its name is unique. But in the context of a Graph it does, because it is an invariant of the Graph, not the Node. So the Graph is responsible for protecting this invariant.

As for the 'god aggregate root', it's not uncommon to have a single aggregate root from a global business perspective. But an important aspect of DDD is identifying the different contexts within the system. Your system may have a high-level root containing many Graph objects. In this high-level context of managing your graphs, you're probably not even interested in the low-level Link objects in the graph.

It is important that you model your domain objects according to the context. This was one of the most important things I've come to realize in the last few months. Most people know about DDD because of repositories, or maybe because of entities and value objects, but these aren't nearly as important as bounded contexts.

Even though there is only one business concept of Something, it is perfectly fine to have multiple models that represent this concept of Something, a single implementation per context. One implementation might be an aggregate root, while the other implementation is just part of a larger aggregate, all depending on the context.

Common software mantras are about code reuse, DRY and the likes, so at first it felt wrong to have multiple classes that represent the same business concept. But once I was able to let go of this feeling and realize that each implementation had its own responsibilities, it made things so much easier :)

Aligarh answered 31/7, 2013 at 19:36 Comment(4)
"Most people know about DDD because of repositories, or maybe because of entities and value objects, but these aren't nearly as important as bounded contexts." - +1 Well put sir.Proglottis
Wow, so this has kind of blown my mind. I had been under the impression that BCs were basically for different applications that form some subset of the entire system. Thinking about things like this has significant ramifications for the rest of my domain model. This has given me a lot to think (and learn) about. Which can only be a good thing.Mombasa
Having done a lot of digging around and reading up on BCs, I did come up with a working prototype using this approach. The problem I came across was that there needed to be a lot of communication between the 2 BCs meaning they became very tightly coupled and I began to suspect they were no longer truly BCs. Your explanation about multiple models representing a single concept of Something was a big eye-opener for me though, and is what led me to CQRS, so thanks again!Mombasa
Also don't forget that if 2 bounded contexts have a lot of communication between them - it's quite possible that your model is incorrect.Questioning
M
6

The solution I found to this problem came from taking a CQRS approach. I found an excellent CQRS introduction here.

In my 'write' model; Graph, Node and Link are all Aggregate Roots, but names are entirely managed by the parent collection. Thus in the write model, a Node has no idea what its own name is (meaning name updates have to go via the owning Graph). In the corresponding 'read' model, the name is associated directly with the Node (since this is useful for display).

The advantage of this solution is that it allows me to keep my ARs small, but since the 'name' info is held inside the parent collection, I have no issues with maintaining cross-aggregate invariants.

Mombasa answered 6/8, 2013 at 15:9 Comment(2)
Good to hear you were able to solve your problem. I've read about CQRS as well, but never really put it into practice. But maybe I should, it would sure make parts of my projects easier to implement :)Aligarh
Do you have a graph.renameNode(node, name) or a node.renameChild(childNode, name) method? What is the exact implementation by getting and setting the node names? Where do you store the names? Are they node attributes or are there in collections, e.g. name -> node maps?Asiatic

© 2022 - 2024 — McMap. All rights reserved.