Design your events granularity
What events should my component publish? Not always an easy question to answer.
Software design is a continuous task. You never finish a software project, because it needs to evolve as the team’s knowledge about the problem it solves grows. This is why one of the principal characteristics of good software is being easy to change.
In an asynchronous software architecture, the events that a component publishes define its contract with the other components in the system. And a good interface contract is one that lets the component be useful to the other components, while making it easy enough to modify as the project grows.
In this article I will review three different approaches a team can take when definining the events published by a component in a software system. There is no perfect solution, they all have advantages and disadvantages. But having the changeability principle in mind, I’ll provide a general recommendation that can be used as a rule of thumb.
Example system
Let’s use an example system to illustrate our thinking, a typical e-commerce site with the following services:
- The shopping cart service
- The orders service
- The logistics service
- The notification service
We will start the discussion assuming that certain decissions have already been made.
In this system, the users browse the web page using the shopping cart system to add and remove products, and to configure payment and delivery methods. When the order is placed, the shopping cart service calls synchronously the orders service to create the new order. Once the order is persisted in the database, the orders service publishes a new OrderCreated event that the logistics service will listen to start the packing and delivery process.
Everything is pretty straightforward so far. Now we need to decide what events the logistics service will publish so that the notification service will keep the customer updated on their order’s delivery state.
Minimum granularity: expose high level changes
The smallest possible granularity would be for the logistics system to publish an OrderUpdated event each time a modification is made to the order.
Main advantage of this approach:
Minimum exposure of the logistics internal model. By sending only the order id as payload with the event, we are safe to modify the internal behaviour of this component, including the different states an order can be and how we represent them internally. Anyone listening to it will need to fetch the current order status by calling the appropriate endpoint.
Main disadvantage:
This approach makes it very difficult for the notification service to detect when the order status has changed. We would probably need to store locally in the service the previous state so we can identify whether it was an status change and what the transition was.
Maximum granularity: expose every change
On the opposite side of the expectrum, we could publish each field modification individually in its own event, having events like OrderStatusChanged, OrderDeliveryMethodChanged, OrderDeliveryAddressChanged and so on.
Main advantage of this approach:
This makes it much easier for the notification service to identify when a change in the order requires notifying the customer.
Notice that when the OrderStatusChanged event is received, it will still need to verify which is the new status in order to send the corresponding notification template.
Main disadvantage:
This approach fully exposes the internal representation of the order within the logistics system. Every field, every modification, is revealed to the exterior, creating a contract that is very expensive to honor for the component in terms of future changeability.
UseCase granularity: expose meaningful change
So, we have covered exposing all the details of the internal representation, as well as only the high level modifications, what is left?
Well, let’s take a step back and think again about our goal. This software manages orders, and the notification component is responsible for sending emails, SMS messages, push notifications, etc., to users when something important happens with their order.
There are things that will probably change forcing us to update the logistics component, like the volume of orders, the available delivery companies, or the number of countries supported. New workflows may emerge, such as dropshipping or selling food that requires refrigerated delivery trucks. A good software should be able to adapt to all those changes.
But there is something that has a much lower probability to change, the use cases:
- when an order is shipped, the customer has to be notified.
- when an order is awaiting for pickup, the customer has to be notified.
- when an order could not be delivered, the customer has to be notified.
- when the order was delivered, the customer has to be notified.
- …
So our best bet is to map our events from our use cases, instead of the changes in the data:
Main advantage of this approach:
Completely hides the internal representation of the logistics service, by publishing the use cases instead of the changes in the data model.
Besides, the notification system will have a much clear catalog of events to react to.
Main disadvantage:
There might be some cases where a system needs to react to a change in the system that is not exactly a use case. There will always be corner cases that require special handling.
General advice
As a general rule, focusing on use cases is the safest approach when defining the system’s events. Instead of using events to communicate changes in the data, report executed use cases.
This approach keeps the internal structure of your services private, giving you greater freedom to change them as your needs evolve.