1
votes

I have a question regarding handling multiple levels of nesting of child entities within an aggregate root.

Up until now I have only been dealing with aggregates roots with no children, or at most "one level" of nested entities.

When creating and modifying the child, I've managed that through the AR ie. Using traditional Order / OrderLines example:

class Order:
public void addOrderLine(product, price)
public void adjustPriceForOrderLine(percentage_change, line_id)

I've just designed an AR with 2 levels of nesting(both 1-to-many), and am having a hard time determining an approach for handling interactions through the AR:

class Root:
public void addLevelOneChild(...)
public void adjustLevelOneChild(...)

but when it comes to working with the child nested beneath the LevelOne child, the approach I've been taking becomes more verbose.

public void addLevelTwoChildToLevelOneChild(..., levelOneChild_id)
public void adjustLevelTwoChildInLevelOneChild(..., levelOneChild_id)

It works. But always requires effort to determine the local identifier of the Level One child before any action can be taken on the Level Two Child.

Also, when creating a new levelOneChild with a factory method, I need to either return the local id for the levelOneChild to then create a levelTwoChild, or jump through some hoops to get the local identify of the new levelOneChild.

public local_id root.addLevelOneChild(...)
public root.addLevelTwoChildtoLevelOneChild(..., local_id)

or

public void root.addLevelOneChild(...)
public local_id getIdforLevelOneChild(some natural identifier(s))
public root.addLevelTwoChildtoLevelOneChild(..., local_id)

Does this seem like the right approach? Or any suggestions for a more elegant solution.

I have been thinking of using natural IDs (I'm currently using a guid just for consistency) for local_id, which would help alleviate the need to return or query the generated key. Although this is letting persistence implementation details leak out.

Thanks

3
Just realised that my last thought could be the approach to take. Ignoring ids entirely means that a method: addLevelTwoChild(...) where the parameters also include the local identifiers to the LevelOneChild. The AR will then handle internally adding the LevelTwo child to the correct LevelOne child. - Steven

3 Answers

3
votes

The first thing you should ask yourself is whether or not you should have such a large cluster AR and explore breaking it down. What invariants are you trying to protect? Could these rules be eventually consistent instead?

If you must have this large cluster AR, here's a few ideas:

1. Generic hierarchy handling:

    addNode(node, parentId?)    
    removeNode(childId, parentId?)

Note that with an heterogeneous tree this may be more difficult. Also, the encapsulation is slightly broken if you pass a Node to addNode because the entity could get modified by the caller. You could pass an immutable node descriptor instead.

2. Children notify changes to parent:

You could exceptionally allow to interact with entities directly, but have them notify the AR of any changes. For instance, think of the Document Object Model (DOM) and the bubbling of events.

    root.childOfId(childId).addChild(...)

    addChild(...) {
        parent.notifyAddChild(...);

        //add child
    }

3. Hierarchy editor:

editor = root.newHierarchyEditor();

editor
    .firstChild()
    .addChild(...)
    .applyChanges(root); //could call on #1-like methods or root.editHierarchy(mutations)

Note that whichever method you'd choose, you'd have to strive to keep the API aligned with your Ubiquitous Language as much as you can. Also note that if you only have 2 levels of depth then going for an explicit API like you suggested is probably better than any of the above.

1
votes

I attempt to keep the design to one level: an aggregate root with one or more collections of value objects. Those value objects may be a link to another aggregate such as in the case of an OrderLine being the associative entity, in DB-parlance, to the Product aggregate.

One would need to understand the domain to see whether the first level children shouldn't be aggregates in their own right, or if those first level children provide the link to an associated AR which in turn would contain the second level value objects in your design.

However, on the abstract/conceptual level in your example it would be impossible to tell :)

I would suggest doing a thought experiment where, in your design, you are not permitted to go down more than one level: how would you change your design?

1
votes

Nesting of aggregates should be forced by true business invariants only, unless that is the case I like to keep my aggregates small limited to its own attributes and value objects.

Keep in mind that forced nesting of aggregates can negatively impact performance and scalability.

I thoroughly recommend you to read three part series by Vaughn Vernon Effective aggregate design.

Happy DDDing!