Not all Events are Created Equal: How to Choose the Right One for You

Mario Bittencourt
SSENSE-TECH
Published in
8 min readAug 11, 2023
Photo by Matt Walsh on Unsplash

When leveraging event-driven architecture, publishing and consuming events become a key part of your application. While the concept of what constitutes an event is meant to be simple, you do have choices on how to define them.

Domain events, integration events, thin events, summary events, and event-carried state are some of the terms found in the literature, but what do they mean? Which one should you use? And what are the trade-offs?

In this article we will explore some of these topics and provide insights to help you choose the right one for you.

What is an Event?

Webster’s dictionary defines an event as “something that happens or takes place, especially a noteworthy occurrence”. Once it happens it becomes a fact, an immutable piece of information that, when pieced together, helps us understand the evolution of a given context. In the software development world, events are messages a given system/service emits as interactions or state changes.

Figure 1. Event stream.

These events can be used by any system, including the originating one, to trigger additional reactive behavior.

Figure 2. Events shared may trigger additional actions in the consumer.

Once your events have been established, what should they contain? Let’s define a context example and look at some alternatives.

Example: Fraud Case

Imagine that one of your services handles fraud detection as part of your e-commerce solution. Once an order is placed, and before being fulfilled, the order is assessed to identify potentially fraudulent activities.

Your service can assume one of the following three states: received, declined, or accepted.

Figure 3. Evolution of a fraud case.

Once it has been received and evaluated, it can either be declined, indicating the reason, or accepted. In both cases a confidence score is provided allowing you to make a decision and even take additional steps depending on the severity of the response.

Your events would be FraudCaseReceived, FraudCaseDeclined and FraudCaseAccepted.

Thin Events

A thin event contains only the minimum information required to indicate what transpired and which of your business entities was affected.

In our fraud service example, this would translate to:

In our example, the identifier is the fraud case number that the service generated and the resulting state (received, declined or accepted).

The thin event exposes very little of the actual entity, which has two consequences:

  • It introduces low coupling between the consumer and the producer.

We are not exposing any internal representation of the fraud case in the event. In theory, this allows us to evolve its representation without affecting consumers.

  • It requires us to provide a way for the consumer to retrieve more details about the fraud case.

If the consumers require more than just the entity id, we have to provide some way for them to retrieve this information. This means consumers will “phone home” to get the information they need.

Figure 4. Consumers will reach the producer to find more information.

When to use Thin Events

Low coupling is a desired trait in our systems, so thin events have that in their favor. In some cases, just the id and the state may be enough for the consumer, making it an interesting choice.

If you need more information, the “phone home” aspect can be either neutral or negative. It will be neutral if, in the end, it is a simple API on top of your entity repository. You still have to guarantee its availability and scalability as it is now a dependency for the consumers, but this may be a trivial task if you have a small number of events and consumers. But there’s a catch! Due to the asynchronous nature of consuming your events, by the time the consumer reaches back to obtain the details they may have already changed.

Figure 5. By the time the consumer requests the aggregate state it may have already changed.

You need to verify if this may be an issue within your domain. If it is, then one of the following two options may be better suited.

Event-Carried State Transfer

In this case, our event contains the complete definition of the entity after it mutated as a result of an interaction.

Because it is a complete event:

  • It can introduce unnecessary coupling with the consumers.

Consumers will receive a payload that may contain more than what they care about

  • It is self-contained.

Since it contains the entire state, there is no need to reach back to the originating service.

When to use Event-Carried State Transfer

Because we do not need to reach back to the originating service, this approach removes the temporal coupling between the two parties involved. Since the consumer is asynchronous, by the time it consumes the events, the originating service may even be offline, which does not affect its processing.

If you have many services consuming your events and/or you do not want to have to maintain a retrieval mechanism, this is a good choice. Some interesting uses are when you want to build a cache of an entity or feed a data source to enable non-transactional usages.

What makes this type of event a liability is when you have consumers that start creating dependencies, some incorrect, to the contents of the event. They can start making assumptions about the lifecycle of its content and disseminate it further within their own events.

Event-Sourcing

While it is referred to as a data pattern, a characteristic of events emitted in an event-sourced solution is that they only contain what has changed between the previous and current states.

This places Event-Sourcing somewhere in between the previous two approaches:

  • It contains more than just the id of the affected entity, but not the entire state.

Consumers don’t need to look back to the origin to find what has changed.

  • It does not contain the entire current state of the entity.

If consumers need to make a decision based on the entire state they must store the original state and keep evolving with each new event they receive.

When to use Event-Sourcing

Because each event contains more than just the id, the temporal coupling is averted, having similar benefits to the event-carried state approach.

The fact that the event only has what changed can either be neutral or negative. If consumers can decide based on the current event, then this approach is favorable. On the other hand, some consumers may need to compare the current changes with the previous state. If that is the case, it becomes a negative trait as it requires the consumer to understand and consume all events in order to build the state.

In general, I see this approach as only beneficial to be consumed by the same bounded context that emits the events. A common usage is when you also adopt CQRS.

Now that we have looked at some options and how to assess each positive and negative aspect, let’s see what else should be part of your event, independent of the approach taken.

Bonus

When we look at the approaches, we find that there is standard information that is always present:

  • The identification of the entity that mutated
  • What happened

Together, they inform the consumers about what happened and to whom it happened. But there are some other useful entries you should consider adding as they can help the consumers.

Occurred At: Events are facts about something that happened in the past. They will be used asynchronously sometime in the future. Having an indication of when they happened is important for consumers as it can affect their reactions to it.

While most consumers may be a few seconds behind, if they have to perform any maintenance — or if there was a sudden spike in the volume of events — they will find themselves with a backlog of events. If the date is relevant, then consumers will consider when they received the event as the date it happened, leading to incorrect results.

Emitted At: This is when the messaging infrastructure actually receives the event to be emitted. Most solutions add this automatically to the message metadata (also referred to as envelope). This can allow you to monitor and potentially flag if the gap between occurred at and emitted at grows too big.

Version: A basic understanding of event-sourcing is that each time your entity mutates you end up with a new version of it. But this is not exclusive to event-sourcing so consider exposing this with all events. It will help the consumers understand, and even detect, out-of-order events by looking at the version field.

One Size Does Not Fit All

Event-driven architecture is a powerful approach that has its fair share of benefits and challenges. Once you decide to use it, the next step will be to define the event format.

As we saw, there is base information recommended to be available in all of them, and at least two major approaches, each with their own set of advantages and potential pitfalls.

When assessing your reality you may end up in a situation where you have more than one approach in your services, with some emitting thin events and others state-carried events.

Unless you have specific requirements, I recommend starting with thin events and evolving from there if the nature of the producer/consumer relationship is better suited by state-carried events.

An approach not covered in this article is the summary event, which has the appeal of controlling the exposure you have to external domains if you have a lot of granular events emitted. It is a valid approach if you are worried about a high contextual coupling with the consumers.

What approach are you currently using?

Editorial reviews by Catherine Heim & Sam-Nicolai Johnston.

Want to work with us? Click here to see all open positions at SSENSE!

--

--