Not All Commands Are Equal (And Neither Are Events)

Posted on 30 November 2024

Usually, in an event-driven architecture, events are emitted by one service and listened to by many (1:n). But what if it’s the other way around? If one service needs to listen to events from many other services?

When we talk about communication between services, we distinguish between commands, queries and events.
For events and queries, let’s say we agree on these attributes:

Queries, Events
Type Event Query
Describes.. An event that has happened in the past A request for information about the current state of one or many objects
Expected Response None The requested information
Communication Pattern Fire-and-Forget Request-Response

I don’t expect controversy here. For a query, you want a response. And on the other hand it’s well established that events describe facts. And that events are emitted, with no knowledge required what’s listening to the event and reacting to it. And certainly no response is expected.

But what about commands?

Commands
Type Command
Describes.. An intention to perform an operation or change a state
Expected Response ???
Communication Pattern ???

Let’s agree that a command expresses intent, it’s a request to do something. An example could be a request to cancel an order (trigger e.g. by a request from a user). This can fail, in multiple ways: The referenced order might not exist, it might be already fulfilled and no longer cancelable, or the user might not be authorized to cancel it. That’s why a command typically yields a response: An acknowledgment that is has been executed successfully, or an error message. Consequently, the communication pattern is request-response. The caller will need the response to decide on the next action.

This is the kind of command I usually mean when I talk about commands (which I do a lot, even publicly). But while doing so, I learned that a lot of people disagree with the association command = request - response. So let’s differentiate, and call commands that expect a response “ask commands”.

"Ask" - Commands
Type "Ask" - Command
Describes.. An intention to perform an operation or change a state
Expected Response A confirmation that the command has been executed, or an error message
Communication Pattern Request-Response

Again, this used to be the only type of command I cared about, so that’s how I defined it e.g. here.

It has been brought to my attention though, that there are cases were there’s no interest in the response. Possible scenarios are:

  • The sender doesn’t care if the command is successfully executed or not, or
  • the receiver guarantees that the command will be handled, and it will take care of any error handling itself, or
  • executing the command will result in an event that indicates how the command processing went, that can be listened to by anyone interested.

An example for a command that doesn’t yield a response could be sending notifications. Imagine you have many subdomains that emit events that users are interested in, and should be informed via some sort of message (text message, e-mail,..). Using events from the originating domains, the notification service would need to understand a large number of different event types and make sense of them. Any additional service that also triggers user messages will require an extension of the notification service as well.
To avoid this, just one command type could be defined. Although the processing could fail, the sender would never be interested in this. The notification service itself would take care of any error handling, this would not go back to the originating domain.

So instead of asking another service to do something (which it can reject), we are telling another service “take care of this, you handle it, I don’t want to hear about it again”.

Let’s call this type of commands “tell commands”:

"Tell" - Commands
Type "Tell" - Command
Describes.. An intention to perform an operation or change a state
Expected Response None
Communication Pattern Fire-and-Forget

An Extended View of Commands

Let’s review what we talked about so far:

Events and Commands with extended criteria
Type Event "Tell" - Command "Ask" - Command
Describes.. An event that has happened in the past An intention to perform an operation or change a state An intention to perform an operation or change a state
Expected Response None None A confirmation that the command has been executed, or an error message
Communication Pattern Fire-and-Forget Fire-and-Forget Request-Response

So in terms of the overall flow, in terms of the structure of communication, “tell” has more in common with events than with an “ask”.
To solely rely on the naming to separate them is questionable. It’s easy to take “asks”, expect a response, but just name both requests and responses like events - this is what Martin Fowler calls “using events as passive-aggressive commands”. Just like those events should really be commands, maybe “tell” commands should really be events?

People who use and like “tell” commands will suggest other categories for distinction:

1:n vs. n:1
Suggested criterion: Events have one sender, and any number of receivers. Commands have one receiver, and any number of senders.

Model ownership
Suggested criterion: The schema of an event is defined by the sending service, the schema of a command by the receiving service.

We can extend our table to incorporate these criteria, if we want to show that “tell” commands and events are not the same:

Events and Commands with extended criteria
Type Event "Tell" - Command "Ask" - Command
Describes.. An event that has happened in the past An intention to perform an operation or change a state An intention to perform an operation or change a state
Sender:Receiver 1:n n:1 n:1
Schema Owner Sender Receiver Receiver
Expected Response None None A confirmation that the command has been executed, or an error message
Communication Pattern Fire-and-Forget Fire-and-Forget Request-Response

But - who says that for events always have to have the schema defined by the sender, and sender/receiver is 1:n? Let’s explore further.

Events vs. Tell-Commands - different approaches compared

Let’s come back to our notification example. A possible approach would be for the notification service to listen to events from domain services and make sense of all of them:

The notification service needs to understand all sorts of events.
The notification service needs to understand all sorts of events.

This is nice and event-driven. But as mentioned above, this requires the notification service to be aware of all sorts of domain events and handle them, and to be extended if more are added.
To avoid this, let’s consider “tell” commands as an alternative:

The notification service understands commands.
The notification service understands commands.

You could argue (as I tend to do) that these are not “real” commands, because they are treated like events, i.e. there is no way of indicating success or failure to the sender (i.e. if you consider “ask” commands to be the real commands). And moving from “tell” commands to events is just a change of perspective. The problem caused by the domain events, that the notification service needs to understand all event types, can be remedied by introducing a new, shared schema that all notification events need to implement, so they all adhere to a given interface required by the recipient.

The notification service understands one type of events.
The notification service understands one type of events.

Given we divided commands into two sub-categories, it’s only fair to do the same to events as well. Let’s call our original events, where the schema is owned by the source, “supplier events”. The other kind, where the events conform to a schema provided from the outside, would then be “conformist events”. So the final version of our comparison table is this:

Events and Commands with extended criteria
Type "Supplier" - Event "Conformist" - Event "Tell" - Command "Ask" - Command
Describes.. An event that has happened in the past An event that has happened in the past An intention to perform an operation or change a state An intention to perform an operation or change a state
Sender:Receiver 1:n n:1 n:1 n:1
Schema Owner Sender Receiver Receiver Receiver
Expected Response None None None A confirmation that the command has been executed, or an error message
Communication Pattern Fire-and-Forget Fire-and-Forget Fire-and-Forget Request-Response

So what to use?

Commands, Events — while you’d think the boundaries are clear, the classification is quite coarse-grained. E.g. using commands can lead to quite different outcomes, depending on if they require a reponse or not.

So in the n:1 case, if one service needs to listen to events from many other services, what should we use? For commands an n:1 relationship is the common case. And we saw above that you can have commands without responses, giving you many of the advantages events have. So should you prefer “tell” commands or “conformist” events?

My personal preference is on conformist events. To me only “ask” commands are really commands, and “tell” commands are just events in a confusing disguise. I don’t think the schema ownership or sender/receiver ratio should force you to think in commands instead of events. I want my systems to be event-driven, want to reason about things that happened, use event storming to figure out what to do.

But I’m still learning (and hopefully always will be). Until recently, tell commands weren’t even on my radar. So I’m looking forward to learning more about the readers’ approaches, and to writing further blog posts sharing new insights in the future.

Categories:
  • Software Architecture, Event-Driven Architecture

Share this article on: