8 February 2020
When is an event not an event?
Message design in an event-driven architecture can be quite nuanced. Just because you call something an “event” it doesn’t follow it will help you to achieve loose coupling and the associated benefits of scalability, resilience and flexibility.
So, when is an event not an event?
When it’s a message
Messages are the basic unit of communication that message brokers and buses work with. They can be literally anything you want: a string, a number, an object, a command or an event. Messages have no special intent, which makes them less meaningful than events.
An event is distinguished from a message because there is clear intent, i.e. it describes a change in state. Without this, you can’t really call it an event.
When it’s an entity
An event should represent a change in state, but it should also be aligned to something that has happened in the real world – e.g. an order being placed.
Events are an abstraction rather than an expression of a data model. Sending events in response to changes in entities is a kind of leaky abstraction where you are broadcasting the underlying implementation detail. You end up with a very anaemic set of generic events where it can be difficult to ascertain the underlying business process.
When it’s a request
Events are based on a publish\subscribe style of interaction where the sender does not expect a response from any consumers. This allows services to collaborate while minimising coupling.
There are times when you need to support a request\response style of interaction between services. You can model this with a sequence of events that represent requests and the possible types of response. This tends to make for a complex and fragile implementation, while the expectation of a response creates coupling between services.
Events don’t add value in every situation. If you really need to model a request\response interaction, an API might be a better solution.
When it’s a sequence
Events should be asynchronous in that they shouldn’t need to be processed in a specific order. If you try to assert an order for events you are effectively coupling services together via a sequence. Message ordering is technically possible, but it adds complexity and undermines scalability.
Many messaging technologies can provide an ordering guarantee, but this only applies to the order in which they receive events. If you want to send and receive events in parallel, there’s no guarantee that they will be processed in the correct sequence.
Most of the time you can design out any ordering. If you really need it then you can add a sequence number to the event data so consumers can avoid stale data. This requires establishing a reliable counter on the sender as well as a mechanism on the consumer for persisting the most recently processed event. Not trivial.
When it’s too generic
An event should be specific so it’s clear what business process it represents. You shouldn’t create generic events where the intent is determined by a series of status flags. This creates too loose a contract that places a burden on the consumer to determine what the event means.
For example, an order could be updated as part of several different business processes. Combining all of them into a single “order updated” event undermine clarity. The consumer is forced to deduce from the event data what has just happened – e.g. have items been added to the order, has the order been shipped or has a payment been made?
When it’s a command
A lot of event-driven architectures also support commands. These tend to be implemented using the same underlying technology, but there are key differences that are more than just semantic.
A command is a one-to-one communication between a sender and receiver, while an event can be received by any number of subscribers. A command is used to invoke a process in another application, where an event describes something that has already happened.
Commands can add coupling as the sender has to know something of the responsibilities of the receiver. A well-designed command should mitigate this by decoupling the process that invokes the command from the process that receives it. Note that a receiver can always refuse a command which helps to ensure that services retain their autonomy.
You can replicate this kind of exchange using events, i.e. use an “order processing requested” event rather than an “process order” command. However, you’re not really modelling any meaningful change in state as the intent of the exchange is to request some processing. Treating commands separately can make it easier to track whether they have been refused or successfully processed.
When it’s incomplete
Events are autonomous in that they shouldn’t depend on information in another event. You shouldn’t need to delay processing of an event because you’re waiting for some data to turn up from somewhere else. This implies that events should contain enough data for you to be able to process them fully at the time you receive them.
This can give rise to quite large events and a fair amount of repetition, but that shouldn’t be a problem. The consumer also has a responsibility to be tolerant of missing data. You can always insert placeholders for missing data if it’s likely to arrive through another channel.
When it’s a method call
There should be some cost associated with creating and sending an event. If you have too many of it can create “chatty” interfaces that undermine the boundaries between services. If you use events too freely then can start to feel like method calls. This can start to place quite a burden on the downstream systems that have to receive and process these events.
When it exposes too much implementation detail
An event should be an abstraction that represents a real-world business process. For example, an order gets placed or a payment is made. This is important as it prevents the event from exposing any detail of the underlying system. This kind of information leakage creates coupling as services become too aware of each-other’s internal workings.