Calling cross-aggregate calculation function to update read model after a command applied

565 views Asked by At

I'm new to CQRS and need advice on the following situation in my design. A command updates state of an aggregate A; the read model needs to be consequently updated with a result of a cross-aggregate calculation method; this method belongs to another aggregate B which holds a reference to the aggregate A; the method is a function of states of both aggregate B and the referenced aggregate A. Where is the correct place for this function to be called?

My considerations (can be skipped):

  • Command handler updating state of aggregate A could technically fetch aggregate B from the repository, call calculation on it and put result in the domain event; however I believe it's not command handler's job to fetch aggregates other than one being modified, even for reading purposes; also it's not command handler's job to perform calculations just to send with events rather than modify the state of domain.
  • The domain event ('Aggregate A updated') raised by the aggregate A contains only updated state of aggregate A, there's not enough info on state of aggregate B. Read model's event handler has no access to domain model, so it can neither fetch aggregate B nor call the desired function on aggregate B to update read model.
  • I know that any state needed by command which is external to the aggregate being modified must be passed along with the command. This way the application service, before sending the command, could fetch state of aggregate B (from read model), and put it in the command. For that I would have to move the function from aggregate B to some service and pass there states of both A and B. That would make aggregate B more anemic. Plus the above mentioned problem with doing calculations within command handler.
  • I've read people suggesting that any calculations that only read model is interested in belong to the read model itself. So the read model's handler of my event would just have at its disposal all needed state and behavior to perform calculations. However that would mean I have to duplicate much of the domain model concepts at the query side; it would be too complex to have a full-blown read model.

I've just thought of the following solution: within the domain, create a handler of the domain event 'Aggregate A updated'. It would fetch aggregate B, call the calculation method on it, then raise an 'Aggregate B function result changed' event with the new calculation result in it. Then the read model is able to take the result from this event and update itself. Would this be ok?

Note just in case that I'm not using Event Sourcing.

Any thoughts on this situation would be much appreciated. Thanks!

UPDATE: making the situation more concrete

My aggregates are Workers (Aggregate B) and Groups of workers (Aggregate B). Workers and Groups are a many-to-many relationship. Imagine both a Group and a Worker have some Value property. Worker's calculateValue() is a function of the Worker's Value plus Values of all Groups the Worker participates in. The Command described above is modifying Value for some Group. As a result, all Workers participating in the group would return different result of calculateValue().

What do I want from the read model? I want a list of Workers with calculated Values (that already account for Values from the Worker's all groups). I don't even need Group at the read side. If I go the 'do calculation on the read side' way, I need Groups as well as the whole structure of relationships there. I'm afraid it would be an unjustified complication.

1

There are 1 answers

3
Constantin Galbenu On BEST ANSWER

Command handler updating state of aggregate A could technically fetch aggregate B from the repository, call calculation on it and put result in the domain event; however I believe it's not command handler's job to fetch aggregates other than one being modified, even for reading purposes; also it's not command handler's job to perform calculations just to send with events rather than modify the state of domain.

This is not OK because events should represent facts that happened in regard to a single Aggregate.

I know that any state needed by command which is external to the aggregate being modified must be passed along with the command. This way the application service, before sending the command, could fetch state of aggregate B (from read model), and put it in the command. For that I would have to move the function from aggregate B to some service and pass there states of both A and B. That would make aggregate B more anemic. Plus the above mentioned problem with doing calculations within command handler.

You should not send the Aggregate state in an event. In fact you should not query the Aggregate or use it't internal and private state in any other way but by the Aggregate itself. In CQRS the Aggregate is not to be queried. That's a read-model's purpose.

I've read people suggesting that any calculations that only read model is interested in belong to the read model itself. So the read model's handler of my event would just have at its disposal all needed state and behavior to perform calculations. However that would mean I have to duplicate much of the domain model concepts at the query side; it would be too complex to have a full-blown read model.

This is the way to go. However, what do you duplicate anyway? Is the result of that calculation used by the Aggregate to accept or reject any of its commands?

If yes, then it should be done inside the Aggregate, at command execution time and possible the final result sent along the event but only if the calculation can be done with the data from the command and/or the internal Aggregate's state and not by cross Aggregates state. If an Aggregate needs data from other Aggregates then that is a sign that your Aggregates boundaries might be wrong.

If not then the calculation should not stay inside the Aggregate, but only in the read-model.

In CQRS, by splitting the Write from the Read model you would split the calculations also to Write and to Read but there are some cases where a calculation is shared by the two models. In these cases you can extract the calculation inside a Class and use that Class in both the models.