Design of Event Structure
In implementing an event-driven architecture, you should have a clear reason for using events. Equally crucial is deciding what data should be carried by these events. This decision can influence future architectural choices, including approaches to schema evolution, ensuring system consistency, and determining the event-sending mechanism.
What is an event?
We should have a clear understanding of what an event is and its classifications.
In the context of event-driven architecture, an event is a record that indicates something happened, most likely changing the state of the system. While the change may not occur in the emitting system, in fact, if an event is needed, it will be finally processed and change some data.
Several examples:
- An event that changes the state is the
OrderPlaced
event, which indicates that a user placed a new order. - An event that doesn’t change the state of the emitting system is
OperationAuthorizationFailed
. Emitted by the system responsible for executing operations, this event doesn’t alter the system’s state. However, when received by a security analysis system, it triggers the creation of a security incident.
From the consumer’s perspective, it generally doesn’t matter if an event changes state or not. It is just a record that should be processed. But from the producer’s perspective, the first case will require ensuring coherence between events and the state of the system.
Event Architecture Patterns
Let’s remind ourselves types of event architecture by Martin Fowler from his article What do you mean by “Event-Driven”?:
- Event Notification — a minimalistic event that carries information that something happens in the system. It doesn’t contain any information about the state of the related entities. It is used to notify other systems that something happened, and those systems should fetch more data required for processing the event.
- Event-Carried State Transfer — an event that is able to carry data needed to react to the event, so other systems don’t need to fetch additional data. Just because it’s not clear what data is needed, it is better to fetch all data that may be needed.
- Event-Sourcing — a concept that means that entity state is stored as a sequence of events. It should contain at least data to restore the state of the entity.
Regarding event-sourcing, it is worth mentioning that it is a powerful concept but also pretty complex. The main reasons to use it are:
- Providing audit log
- Providing versioning
- Ability to correct state (as example in the saga transactional pattern)
However, it’s a misconception that only event-sourcing can achieve the first two points; Event-Carried State Transfer can also accomplish them. The unique feature of event-sourcing is its ability to correct state. In order to be able to recalculate the state based on the modified sequence of events, it is necessary to keep in the event only the data that was changed by the operation. It is a significant limitation, as it bound us to the concrete type of event.
Data Completeness
From a data completeness viewpoint, the types can be ordered as follows:
- Notification — includes the identifier of the changed entity, the event type, and a timestamp.
- Domain Event (Event Sourcing Event) — contains data changed within the operation’s scope.
- State Transfer — carries most of the entity’s data, sufficient for creating a copy or reacting to the event.
I use Domain Event as the second type because it is used in my current project. Honestly, I’m not sure what is origin; why we replaced Event-Sourcing Event with Domain event. But in general, it helps to separate it from the Event-Sourcing concept, as similar events could be used without Event-Sourcing.
Scope of Processing
Events could be processed in two ways:
- One-by-one — when we have only one event while processing. It could be used with any type events but perfectly fits with Notification and State Transfer events. For notification, it mostly has no sense to have more than one event processed at once because you will fetch the current state of entities from the external system, and it doesn’t matter if entity was changed before. For State Transfer, it is also not required to process more than one event at once, as you already have full state of entity.
- Event log for entity — when we have all events for the particular entity. It could be beneficial for the domain events, as it allows us to build an entity state without problems with order and duplicates.
Out of the box, most tools allow working with One-by-One processing. Lookups for other events for the same entity may require additional effort and cause performance issues. Hence, we should consider the scope of processing when deciding on the event type.
Using the domain events with One-by-One processing is challenging. If you lose the order, it will be hard to build the final state. You will need to fetch all events and apply them to the initial state or keep modification timestamps for each field to verify if the current event should alter it or if it is already holding a newer value.
Schema Evaluation
Schema evaluation and compatibility for events is a separate and pretty complex topic. I will not cover it in this post, but I should mention that it is a significant factor in choosing the event type.
For notification events, we could assume that schema will not be changed and the whole work to keep the API compatibility will be done on the side of data fetch.
For state transfer events, we have one event that evolves with the entity. It requires coordination between consumers and producers to ensure that the event could be processed after the change.
Probably the most complex case of schema evaluation is for domain events. In fact, it causes some kind of coupling between consumers and producers. It is required to ensure that consumers will know all events that could be produced. Changes of event’s data structure will be rare, but the number of events will grow with the system.
Generally, in domain events approach each operation that changing data requires its own event type, hence you are not able to deploy new operation until all customers will be updated. Additionally, it blocks you to apply data corrections and data migrations, because you will not be able to apply them without a proper event.
Consequences
Examining the implications of each event type:
- Notification
- Pros
- Compact size
- Typically, schema versioning isn’t required
- Cons
- More effort needed for processing
- Slower processing
- Vulnerable to other systems’ unavailability
- Lacks entity versioning and an audit log
- Pros
- Domain event
- Pros
- The event log can be corrected by canceling a particular operation
- The log transparently shows changes
- Provides entity versioning
- Cons
- Difficult to replicate the entity
- Evolving the system is challenging
- Pros
- State Transfer
- Pros
- Simplifies entity replication
- Easier evolution
- Easily handles events in the wrong order
- Offers entity versioning and an audit log
- Cons
- Doesn’t allow for operation cancellation
- Doesn’t specify changes
- Event log may be huge
- Pros
In my practice, I’ve seen that State Transfer is the first choice for most of the cases. But of course, it has some drawbacks. Usually state transfer event doesn’t keep intention of change, so event log is just a log of all states that entity had. This limited insight can pose challenges for analytics and audit logs.
As one example, it will be much harder to create an application that calculates time that order was in particular state. You will need to scan all events to find which event change that field, and only then you will be able to store time when change was done. In contrast, a Domain event, detailing the changes, simplifies this task.
If data fetching is already streamlined and efficient, relying on Notification events can greatly simplify the system.
So, the decision should be pragmatic and deliberate, hope that highlighted aspects will help you to make it.