4

I have a checklist system where we are implementing CQRS/ ES (Event Sourcing). We have a command

updateStatus(taskId: string, status: boolean)

to mark a task or sub task as completed. If I receive a command that a sub task is completed, and all the sibling sub tasks are also completed, I have to mark the parent task as completed as well. So in the example below (subtasks 1-3 of task A):

  • [ ] task A - open
    • [ ] task 1 - open
    • [*] task 2 - completed
    • [*] task 3 - completed

Tasks A and 1 are both open initially and then I receive a command

updateStatus(task1, completed)

the CommandHandler needs to generate an event taskCompleted(task1).

My question is what is the correct CQRS/ ES requirement:

  • Generate a single event: taskCompleted(task1)
  • Generate two events: taskCompleted(task1), taskCompleted(taskA)

In the first option I would expect the consumers of events to see that the aggregate should also update itself to be completed. In the second, the command handle takes care of it.

The major downside of option 1 is more processing for command handlers and their deeper knowledge of the aggregate. Another disadvantage is re-use of the events (e.g. say we have logic for sending an email to task owner when it is completed, with option 2 there would simply be a second event handler which just listens to events and acts on them without knowing the full logic).

The major downside of option 2 is a much larger number of events.

Any suggestions on which is the more correct approach using CQRS/ ES?

3 Answers 3

7

Short answer: You should generate two events.

A single command invocation can lead to multiple events, so generating more of them really isn't an issue. But why exactly would you want to do that in your case? To prevent scattering of responsibility.

In a very basic event sourced project I can imagine there are at least two working parts to your application:

  1. event sourced models,
  2. projectors updating the read side of your application to generate data for reading.

If you generated only one event - that a subtask has been completed, you will now need to introduce logic to your projectors, to make a parent task also completed upon completing all subtasks. You are duplicating domain logic, because the same will also live in your write/domain layer, to complete the parent task aggregate upon completion of all subtasks. On top of that, it's quite likely such logic would be written in a completely different language than your domain, e.g. in SQL if your read models are in a SQL database.

If your application is at the stage I have described (i.e. write side with read side projectors), you might say that duplicating the domain logic is not really an issue. After all, in a lot of projects a SQL implementation may include domain rules, too. The problem becomes more apparent when your application grows and/or is perhaps even split between microservices.

If you add a notification microservice which should notify all watchers of a task when the task is completed, with a single event (of subtask completion) your way of determining task's completeness would once again copy the task's domain logic - checking it's local database whether all subtasks are already completed. What makes this even more complicated, unlike projectors, this microservice is very likely to live in a totally different project, apart from the microservice project containing task management. This makes it extremely difficult to track broken domain logic, which is not scattered across your entire infrastructure.

With two events, marking a parent task in a projector is as simple as doing:

fun changeTaskToCompleted(event: TaskCompletedEvent) {
    database.executeUpdate('UPDATE task SET completed = true WHERE id = ?', event.taskId)
}

and in your notification microservice the implementation is also greatly simplified by only reacting to the TaskCompletedEvent:

fun processEvent(event: Event) {
    when(event) {
        is TaskCompletedEvent -> sendTaskCompletedNotificationEmail(event)
    }
}
2
  • Thanks @Andy! This is a very comprehensive answer!
    – checklist
    Commented Aug 21, 2020 at 11:12
  • So with a command generating multiple events, there will also be several expectedRevision results to keep track of. Commented Sep 5, 2022 at 5:33
2

In addition to the points raised in the answer by @Andy, if you have two events, you can arrange your code such that the check if all sibling tasks are completed is moved to an event handler.

This would make the flow of actions

  1. Command handler receives updateStatus(task1, completed)
  2. Command handler emits event taskCompleted(task1)
  3. TaskCompleted event handler receives event for Task1
  4. Event handler sees all sibling tasks are completed
    • Event handler issues command updateStatus(taskA, completed) to the command handler, or
    • Event handler emits event taskCompleted(taskA)

This way, the command handler doesn't even have to know about the completion of parent tasks when all sub-tasks are completed. That is all handled in a dedicated event handler.

2
  • 1
    This is a good extension on my points, +1, however this would depend on the modelled domain, since by doing it this way you could no longer respect the invariants required by business (if the rule is that upon completing all subtasks the parent task must be immediately completed, too, most likely within the same database transaction). If the rules are looser, this approach is great.
    – Andy
    Commented Aug 21, 2020 at 7:30
  • Thanks for your "upgrade" to Andy's answer!
    – checklist
    Commented Aug 21, 2020 at 11:13
0

The major downside of option 2 is a much larger number of events.

Any suggestions on which is the more correct approach using CQRS/ ES?

Having multiple events for different things that happened is not a downside but improves your design. With that the logic of interpreting the change of the data to express what happened from the business perspective is encapsulated in your service and does not leak outside to several projectors. Andy's answer has already explained this very well.

And of course it is totally fine to generate several events after a single command has been executed. It is an implementation detail how the subsequent events will be triggered.

A SubTaskCompleted event could trigger some other code that checks if the all sub-tasks of the task are completed now and then trigger the TaskCompleted event. But it could also be within the same method that executes the command that you determine both events that should be emitted due to the completion of the sub-task.

Note: I would not trigger subsequent SubTaskCompleted events when the whole main task was completed with a separate user interaction because such sub-task progress is no longer interesting when a whole main task was checked as completed. As events should reflect what really happened in the system if you mark a main task completed with a single click it would not make sense to produce sub task completed events for all corresponding sub-tasks from my point-of-view.

Alltough your question and the answers strongly focused on the events (which is good of course) I just want to point out that I see some potential smell concerning your command:

We have a command

updateStatus(taskId: string, status: boolean)

to mark a task or sub task as completed.

I'm pretty sure that updateStatus does not reflect your business language and thus has no strong meaning in your domain.

I would rather suggest to change your command to

completeSubTask(taskId: string)

This gives your command strong meaning which not only expresses the business logic a lot better but also fits to your events. In addition, I have often seen commands/methods starting with a boolean flag and afterwards being changed to a lot more parameters which makes harder and harder to understand the corresponding business logic.

4
  • Thanks for your answer. I understand that sometimes people look at the theory and say it is good to have many events and it is good design. Look at the situation where completing a task should mark all sub tasks as completed. And say we have 100 tasks in a checklist. That means that marking the checklist as completed will trigger 100 events. And clicking again on the checklist to mark it as open will trigger another 100. All these events will need storage and processing. Multiply by million of users and I hope you see my point.
    – checklist
    Commented Aug 22, 2020 at 7:24
  • What I said was it is not a downside to define different events to express different things that happened. So if something can go through several states I would rather have several meaningful events instead of one Status changed event. And in your case I used as example that SubTaskCompleted event can trigger TaskCompleted event. Commented Aug 22, 2020 at 7:39
  • So it might not be useful that TaskCompleted event triggers 100 SubTaskCompleted events. As a user I would be Interested in completed subtasks until the whole main task was completed. So I assume to complete a main task SubTaskCompleted events will have been triggered before anyway. So no need to trigger 100 SubTaskCompleted events when the main task is completed. Commented Aug 22, 2020 at 7:42
  • And if you complete the whole main task with one user interaction that completes all subtasks that's a different use case anyway. No need to publish SubTaskCompleted events anymore here because no one would be interested in the sub-task progress anymore. I added a corresponding note to my answer. Commented Aug 22, 2020 at 7:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.