Search code examples
domain-driven-designcqrsevent-sourcing

Cross aggregate references: Can a command handler loop among a collection of aggregate ids, or can event handlers dispatch commands?


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 ?

Solution

  • 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.