0
votes

I need to remove a id from collections that are hold by multiple aggregates. Lets say i have a EmployeeAggregate, that contains a collection of Hobbies'id. The aggregate is event sourced.

Lets' say somewhere in the app someone, in a basic crud app handling a Hobbies table, deletes one Hobbie row. How can i reflect the changes in all the EmployeeAggregates ?

Frow now in the event store i have events concerning EmployeeAggregate, events concerning Hobbies (with the HobbieWasDeletedEvent), but nothing to make the EmployeeAggregate handle this HobbieWasDeletedEvent.

Some ideas of what i could do:

  • solution 1: instead of dispatching a command for DeleteHobby, i loop over all Hobbies->getEmployee and dispatch a command for each match, handled by EmployeeAgregate. Drawback: what if the data is huge ? the loop might never end

  • solution 2: i dispatch only one command with an array of all the EmployeIds. Then i do my loop in the command handlers, reconstituting the aggregate for each iteration and calling the remove hobby method. I know that theorically one command => one aggregate, but are we talking about one command = one aggregate TYPE or one aggregate instance ? I'm ok with the fact that a RemoveHobbyFromEmployeeCommandHandler cannot act on something else than a an employee; but can it act on a collection of employees, which are of same type ?

  • solution 3: i do solution one (or solution two), but in a command sourcing way: instead of synchronously dispatching the command, i pass it to an async command bus and lets a worker dequeue them and pas them to handlers. Fire and forget.

  • solution 4: i should not care about notifying the aggregate that the hobby has been deleted, as it is not a domain problem. if business team needs to know that, i will end up with a check on projection side to ensure that all the hobbies ids of the collection exists before writing them in the readmodel. Drawback: if i reconstitute my EmployeeAggregate on the fly just to "dump" it and display it's values, it will still have the deleted hobby id in its hobbies collection. so it won't represent the reality. But is that really a use case ?

  • solution 5: would Sagas be helpful here ?

  • any other idea ?

[EDIT]

  • i came up with another idea inspired by Dnomya's comment (see below): through Employee event handlers, i write a "local" readmodel. it's just a table keeping track of all the EmployeeId / HobbyId association, and it's updated only by some specific events (those who concern employee/hobby relations). Then, when a HobbyWasRemoved is handled, i get all the EmployeeIds from this local readmodel and do something. But what ? dispatch commands for each EmployeeId found ? can a event handler do this ? Isn't it the kind of stuff that a saga does ? But what if this collection of employee ids is huge ?
1
I don't know if it is the right approach for that but it seems that you could a reverse dictionary. For the moment, you have A -> List[B]. When a B is deleted, you want to remove it from every List[B] for each A. If you have a reverse dictionary: B -> List[A] you can know which A you have to update. This dictionary can be a view driven by the event triggered from your aggregate. The downside is that you have a bit of work implement that. - Dnomyar
Thanks Dnomyar. Yes i thouhgt about that, but that would mean this reverse dictironnary would be an aggregate and/or a readmodel; In aggregate case i cannot query across aggregate (between Employee aggregate and HobbieEmployee aggregate). In readmodel case i cannot query directly from aggregate, and cannot rely on a readmodel for data consistancy. Still thinking... - xefiji
Dnomyar actually after having read elsewhere that having a readmodel as a record table somewhere was not that bad, i edited my question to integrate this possibility. but i still think that it is a way of seeing stuffs as in relational model, with join table. And i cannot find anything in the litterature about cqrs/es that talks about the use of that kind of "local readmodel". So isn't there a risk that, because it's easy to do, it be generalized and creates a mess in the domain, with objects or arrays of datas not really concerning the domain itself ? - xefiji
What about having the read model in a separate package than the domain model? I use to have a specific package for domain model, one for commands and one for query. IMHO, it is ok to have a model in the query package. - Dnomyar
Yes it's an idea but i'm still not convinced about the fact that a part of the validity of my aggregates (wow, did i say consistency ?) will rely on some kind of projection somewhere else. Let's imagine that i have to share my event store. In general the events are comprehensive; almost human readable, and it sounds easy to reconstruct the aggregates's states from events. but what if a part of them rely on a table built in the query side from the code of an event handler that we did not share with the store ? for me it sounds like we are giving a huge responsability to the read side. - xefiji

1 Answers

3
votes

One approach is to use a saga/process to simulate a two phase commit to manage the following processes:

Assigning hobbies

  • User issues AssignHobby command to Employee aggregate. HobbyAssigned event is raised.
  • Saga receives HobbyAssigned and issues AddEmployee command to Hobby aggregate.
    • If the Hobby is active then EmployeeAdded event is raised
    • If the Hobby is inactive then the EmployeeNotAdded event is raised
  • Saga receives EmployeeNotAdded event and issues RemoveHobby command to Employee aggregate. HobbyRemoved event is raised.
    • Depending on how critical it is to never have an employee with an inactive Hobby, you can introduce a two-phase commit in this step as well, e.g. first request to add the Hobby and once the Hobby approves, then you can finalize the request with the Employee.

Remove hobbies

  • User issues Deactivate command to Hobby aggregate. Deactivated event is raised.
    • From this point forward, the Hobby aggregate will be marked as inactive. Any requests to add an employee will result in an EmployeeNotAdded event.
  • Saga receives Deactivated event. It loads the Hobby aggregate and issues a RemoveHobby command to each of the Employee aggregates assigned to the Hobby. Each raises a HobbyRemoved command.