Events vs commands in asynchronous communication

Event-driven architecture (EDA) is quite in fashion nowadays. A well-designed EDA decreases service coupling and makes the overall system more fault-tolerant.

by Patrick Zinner

To summarize: In an event-driven architecture, components communicate asynchronously by sending each other event messages. Instead of having a component that commands other components and keeps track of the system’s overall state, each component that listens to an event knows exactly what to do when it occurs. The events drive the process. This architectural style is also called choreography. Just like in ballet, no conductor directs all participants. Instead, the actors react to each other’s movements to time their actions perfectly.

 

But just because you send asynchronous messages via Kafka, RabbitMQ, etc., doesn’t mean you are doing EDA. Often, developers tend to mistake commands for events. Events as well as commands can be sent asynchronously. But what’s the difference? Let’s have a look.

 

When sending a certain command message, one or more producers command exactly one consumer to invoke an action. Since many producers can send a specific command, the ownership of command messages is with the consumer. Commands are usually verbs like SendEmail or ExecutePayment.

 

Whereas when sending an event message, exactly one producer informs any number of consumers that something happened. Since the event already happened, names are usually in the past tense, like ProductPurchased or PaymentFinished. Since the event happens in the domain of the producer, it has ownership of the event.

 

But when to use commands and when to use events? Well, of course, it depends. Let’s have a look at two scenarios.

 

Scenario 1: Sending emails

 

Let’s assume that we have three services – and in their processes – emails are sent to the customer.

In the diagram above, each service sends a command to the message broker and the email service consumes those messages and sends an email to the customer. If a new service also wants to send emails, it just uses the same command, and the email service will happily send the email for the service. Whenever any producing service needs to change the logic on when to send emails, there’s only this change in the service needed.

In this diagram, our three services publish event messages and the email service listens to all three types of events. Now, what happens when a fourth event also leads to an email being sent? The email service must listen to it and act accordingly. This might not scale well; the team maintaining the email service will be busy catching up with the new events sent to them by other teams and their services. On the other hand, if the email service is a template-based system where business owners can easily add email templates that will be sent on certain events, this creates an easy way to add and modify email notifications for any event in the system without the source services being changed.

 

Which of the two is better? It depends, but if the responsibility of creating the emails is with the business services owned by different business units, then each service should know when it wants to send emails, and the email service should know only one thing: How to send them. It should not need to know about the individual events, so the command pattern is favorable.

 

This does not mean, though, that the events OrderPlaced, ItemReturned, or UserRegistered should not exist. They might still be relevant for other services and use cases.

 

Scenario 2: Actions after an order has been placed

 

After an order is placed, accounting needs to add it to its list of transactions, logistics needs to assign the order to a warehouse worker to prepare the order, and our delivery partner needs to be informed about the order so they can pick it up.

Using the command pattern, the order service becomes the orchestrator for the process. It needs to know exactly what should happen after an order has been placed. At first glance, this could be a valid approach. But what if the requirements change?

 

Instead of informing our delivery partner for each order individually, they want us to send them a list of orders at the end of the day. Now, from a technical point of view, it would still work as intended if the delivery service collects all the NotifyDeliveryPartner commands and then, at the end of the day, sends one big notification to the delivery company. But this changes the semantics of the whole command.

 

Also, what happens if the order service crashes after sending the first command message? The order service needs to keep track of what it has already done and implement some retry mechanism for each command. While with three commands it’s not the hardest task, it doesn’t scale incredibly well when additional actions must be performed in the future.

In the diagram above you can see the event-driven approach. The order service notifies whoever is interested that an order has been placed, and it does not care what happens afterward. Its job of accepting an order is done. In the scenario where the amount of delivery notifications is reduced to one per day, the team developing this functionality can change it however it wants. The semantics of OrderPlaced did not change, and there is no need to change anything in the order service. Also, since it only needs to keep track of the OrderPlaced event, error handling is much simpler than using the command pattern.

 

For such a scenario, I would go with the event-driven approach because it concerns only one producer but many consumers. Each service can do whatever it wants after an order has been placed, and changing its logic does not influence the order service at all.

 

Summary

 

With the event-driven and the command patterns, we have two options using asynchronous communication. For each use case, we must think about which pattern fits best. When deciding whether to use the command pattern or the event-driven approach, ask yourself the following questions:

  • Do you want to invoke behavior or notify others that something happened?
  • How many producers and how many consumers are to be expected?
  • Who will be in control of the functionality? The consumer or the producer?
  • Which and how many teams and services will a change in requirements concern?

You can give it a try and find out yourself which is the best approach for your project by answering those questions 🚀