Relationships between aggregates

Hi,

i am curious on best practices to implement relationships between aggregates. E.g. we got two aggregates Persons and Groups. I think i am right when i say this is no case for Multi-Entity-Aggregates as a Person can exist without a group and probably might be in multiple groups. If i want to add a Person to a Group i have to make sure that the Person as well as the Group exist. So i could think of three ways of dooing that:

a) add an validation layer that verifies that the corresponding Person and Group exist before the command to add the Person to the Group is issued.

Pros: no validation in commandhandler so it can easily be placed on the aggregate. This also enables us to use unit testing capabilities of axon.
Cons: no enforcement of the conditions, developers can just send non validated commands of that type.

In the unlikely event that the group was deleted but the query data wasn’t updated at that point in time we will have a corrupted state.

b) add an external command handler hat verifies that the corresponding Person and Group exist

Pros: Commands will always be validated.
Cons. I believe we can’t easily unit test this with the AggregateFixture?
In the unlikely event that the group was deleted but the query data wasn’t updated at that point in time we will have a corrupted state.

c) add two commands one to create the association and one to request it beforehand. The request will go to the Group which will only be able to handle the command if it does exist. If it accepts it will emit an event. That event is received by a Saga - that will handle the association over time - and sends the command to add the user to the group to the user. If the user doesnt exist we could eventually end the saga after some deadline. Otherwise the user will emit an success event. If the Group is deleted the saga will take care of producing a consitent state.

Pros: Conditions are enforced and not vunerable to stale state of read model.

Can be Unit tested with axon capailities
Cons: developers can still accidentally choose to use the Add Command instead of the request command

In the case i didn’t miss something important i would go with c but is there a way to mitigate missuse of the add command?

Kind regards
Nils

Maybe compensating events are the right model here. If the “add X to Y” commands are idempotent, you could have a Saga that listens for events from both sides and sends the corresponding command to the other side, and if it fails to dispatch the command, sends a “remove X from Y because X didn’t exist” command to the side that sent it the original event. You’d then have an audit trail of these failed attempts. That’d let you establish the relationship by sending the initial command to either side.

Didn’t though about that. Thank you. Was just one step away from a Solution.

Is there a way to define multiple associationproperties for an SagaEventHandler so that not all Sagas associated to a Person or Group are loaded but just the one that handles the association between those two that are relevant? As far as i know SagaEventHandler accepts just one associationproperty.

These Sagas should be extremely short-lived, so that shouldn’t be a problem. In fact, you probably don’t need Sagas at all, just standalone event handlers. Something like this (syntax is off the top of my head so this might not compile, but hopefully it gets the general idea across):

`
@EventHandler
public void on(PersonAddedEvent event) {
// Person was added to the group; add the group to the person.
commandGateway.dispatch(
new AddGroupCommand(event.person, event.group),
(command, result) -> {
if (result.isExceptional()) {
// Couldn’t add the group to the person; remove the person from the group.
commandGateway.dispatch(RemovePersonCommand(event.group, event.person));
}
});
}

`

And then a similar thing for the event in the other direction. This setup will end up sending redundant commands but that’s fine as long as the aggregates ignore them; the overhead of loading an aggregate to handle and discard a redundant command shouldn’t be meaningfully different than the overhead of loading a Saga to track the state you’d need to track to avoid sending redundant commands.

Thank you for the suggestion. I would like to go with sagas though as the processes get a bit more complex. So i would like to manage the whole association in one Saga and also react if a group is deleted etc. I got a Problem though.

When a Person joins a Group an Event is emitted:

PersonJoinedGroup(personid, groupid)

When a Person is added to a Group an Event is emitted:

PersonAddedToGroup(personid, groupid)

The managing Saga has an SagaEventHandler(associationProperty=“personid”) that is also annotated with @StartSaga and accepts a PersonJoinedGroup event.
The managing Saga has an SagaEventHandler(associationProperty=“groupid”) that is also annotated with @StartSaga and accepts a PersonAddedToGroup event.

First time a Person joins a group that first handler is called which is perfectly fine:

PersonJoinedGroup(user1, group1) -> creates Saga1 -> Saga 1 sends command AddPersonToGroup(user1, group1) to create other side of association -> group1 emitts event PersonAddedToGroup(user1, group1) -> Saga1 handles that with the first @StartSaga event handler and marks the association as established.

So far so good. But now the Person joins another group:

PersonJoinedGroup(user1, group2) -> Saga1 handles that with the first @StartSaga event handler

Instead i would like to create a new Saga if the group doesn’t match the current association. I also thought about using the @StartSaga(enforceNew=true) but then the bidirectional association would obviously fail. What would be nice to have is to be able to have multiple associationproperties such that a new Saga is created if not all of these match. As thats not avaiable: Can i programmatically trigger that creation in Saga1? Or any other ideas?

PS: oneToManyRelationships (e.g. Person can just belong to one group) could then be easily handled with eventual consistency when adding another SagaEventHandler that matches just one property in this case personid and releases the association if the groupid is different compared to the one associated with the saga.