Event Sourcing with Emmett: Reducing the Entry Barrier

Mario Bittencourt
8 min readMar 12, 2024

--

I am a big advocate of event sourcing, having written about it and using it for years. Despite being around for some time, it is not uncommon to still find developers unaware of it, confuse it with event streaming, or stay away from it due to its perceived complexity.

I recall an interaction with a senior developer who mentioned he was avoiding due to the time it would take to explain all that is involved to any newcomer.

While is definitely a valid statement and can’t be overlooked, should not hinder you from advancing if the tool/pattern can provide true value.

In this article, I would like to try to address the complexity aspect by using a new library called Emmett and hopefully help you consider using event sourcing in your next projects.

I will not cover the theory behind event sourcing. For that, I would recommend checking a previous series of articles on the topic:

Addressing Complexity

Event sourcing is a pattern that is commonly used with others in the development of event-driven applications. I tend to use them with Domain Driven Design (DDD), Hexagonal Architecture, Command Query Responsibility Segregation (CQRS).

While you do not have to use it with the other ones, more often than not it makes sense. To me, this is one of the reasons for the avoidance. Each one of the aforementioned aspects comes with a learning curve to do it right.

To many, this much becomes overload and some of the tenets associated with these patterns are perceived, and usually are, simply overhead. They may add layers of abstraction that prematurely shift the focus and increase the adoption difficulty.

Let’s see how we can balance this using Emmett!

Roadmap

Our journey will entail:

  • Context (Our Problem, Our Entities)
  • Entity States
  • Events
  • Expected Actions (Commands)
  • Business Rules
  • Evolving State
  • Reconstructing State
  • Putting (Almost) All Together

Context

Whenever trying to illustrate a pattern like event sourcing, I tend to see two common examples used: handling transactions in a bank account or an e-commerce shopping cart.

Let’s try to use a slightly different one: Assessing fraud on customer orders

In this simplified example, a fraud case is opened whenever an order is placed. The fraud access is assessed to identify if it is considered fraudulent or not.

If it is fraudulent, the fraud case is declined. If it is not fraudulent, the fraud case is approved.

Additionally, a fraud case can be canceled if the order is also canceled.

In our simple case, we have one entity: Fraud Case

Entity States

Our Fraud Case has a lifecycle that evolves as the result of changes in our system state.

Figure 1. The state diagram of our application.

In the diagram, we can see what are the allowed transitions that our fraud case expects. Later on, we will capture the conditions as part of our business rules.

For now, let’s start coding these states.

Each one of these types represents the state that any given Fraud Case can be over time.

Note how each state is a simple structure and we capture all possible ones in the FraudCase type.

Events

Figure 1 indicates all the events that are emitted during the fraud case lifecycle. So let’s define them in the code.

Here we first start leveraging Emmett’s library by using the Event type provided.

Notice how we define the type for each event, and the data used. We can optionally define metadata fields as well.

Each event only contains the data that represents the change in the state of the entity. So for a fraud case that has been canceled, we only need to know the cancelation reason.

Expected Actions (Commands)

Your application provides some functionality to its users. That functionality can take the form of queries, which return some information, or commands, which if allowed will ultimately mutate the state of your system.

In our case we already saw that our application will allow you to:

  • Open a fraud case
  • Cancel a fraud case
  • Approve a fraud case
  • Decline a fraud case

All commands use the Command type from Emmett’s as well. Each command encapsulates the information that is required to later execute the action.

So far we have defined our states, the events that are generated, and the commands that trigger them.

Now let’s see how we actually handle a command and protect the business rules.

Business Rules

Our actions (commands) will act on the entity’s current state and, if the business rules are met, emit an event indicating the change.

Each action is a separate function that first guarantees the business rules and then returns the event of the specific type.

There are no external dependencies, which makes this easier to test as it is a pure function. We also leverage the types defined to help with the IDE autocompletion.

When we compare it to an OO approach, you can think of each action as the method(s) of your entity.

Evolving State

If an action is accepted as valid and an event is generated, now we have to mutate the current state of our entity.

We will do so by defining an evolve function that takes the current state, and the generated event from the action and updates the relevant properties.

The logic here is simple, you return new data that uses the current state and updates just the necessary pieces of information based on the event.

We saw that our actions require the current state to ascertain if they are allowed or not. But how to obtain that state?

Reconstructing State

In event sourcing, we persist the events and use them to reconstruct the state at any given point. For that, we have a concept of an event store that is where we will persist and retrieve the stream of events for a given entity.

Emmett provides a minimal interface and concrete implementations for an in-memory or EventStoreDB.

Its interface offers 3 methods:

  • appendToStream — which persists the events generated
  • readStream — which retrieves a list of events from a stream (for a given entity)
  • aggregrateStream — which uses the evolve to take a steam of events and return a state

We have come far but how do I actually trigger the action we defined? How the event store is used? Fear not, let’s see how to connect the dots.

Putting (Almost) All Together

To simplify this discussion, let’s forget for now the actual user interaction, which is usually either exposed via an API or via consuming external messages, files, etc.

Emmett provides a high-order function CommandHandler that stitches together all the concepts and pieces we have described so far.

This CommandHandler uses the store to read the stream and pass the events to the evolve function. It reconstructs the current state.

Then we execute the action (or at least attempt to). If it is allowed, the event store is called to persist the newly created events.

How about the getInitialState and mapToStreamId? They are simple helper functions as we can see below

The evolve function always expects to receive a state. But what is the initial state before you first interact with your system? The standard here is to define an initial one to enable the mutation to take place.

The mapToStreamId helps you to build the entityId to the name of the stream in the event store. In my example, I simply provided a human readable prefix to indicate what type of entity the events are related to.

Pretty cool right? But we are not yet over in our exploration.

Testing

When approaching testing you can think in terms of: given a set of events that took place before, when a command is executed, I should generate a new event(s).

Figure 2. Conceptual view of testing an event-sourced application.

Emmet helps us by providing a DeciderSpecification helper.

The given function will receive a list of the events to be applied in order. The when will receive the action (command) you want to test, and the then will list the event(s) that we expect to emit after execution.

It is even possible to specify a thenThrows to indicate if you are trying to perform an action that is not expected to take place given the state of the entity.

The decide function passed to the DeciderSpecification is a simple function that just routes the commands to the right action.

How About External Dependencies?

In our example, we have been able to have all our domain logic implemented as pure functions. We aim to be as deterministic as possible and facilitate testing.

We already have an external dependency in the form of the event store but that serves just as the persistence. So what happens if you have to orchestrate a dependency for which the outcome influences the business rules?

Well, keeping with the functional core, imperative shell approach, we should place it closer to the edge of our application.

We have then two main options:

  • If you have a single way to request the functionality

Imagine you can only serve this via API or messaging.

In this case, you can add the dependency call directly in the UI (Api/Messaging) layer.

You then modify your command to pass the results of the external dependency.

  • If you have different ways (API, messaging) to request the same functionality (or you want to separate the UI from the application code)

Then you could encapsulate the external dependencies into a separate function (or class) and use that to trigger our existing command handler.

In both cases, we keep our actions free of external dependencies. Traditional mocking can be used to perform unit or integration testing for these layers.

Great Scott! How Do I Travel Back in Time?

One of the added benefits of event sourcing is having the ability to specify a specific point in time and see what your entity looked like.

This enables you to reply consistently to clients that may consume integration events after the entity has changed and help with some troubleshooting.

In more advanced cases you may want to test a new implementation for business rules and compare if you would achieve the same end state.

With Emmett this is simple. Imagine you want to retrieve the state for the Fraud Case at a specific version. You simply need to pass that to your event store, like below.

The aggregateStream will return the state as the result of evolving all events from that stream until the specified version, which in our example is version 4.

Conclusion

When I first started to adopt event sourcing, a common discussion was: you do not need a framework to do this.

Indeed if you follow, the principle is simple:

  • take a command, assess its validity, and generate an event.
  • have a functionality that expects the new event and current state and outputs the new state
  • persist the new event(s)

However, the devil lies in the details. Repeating the same code over and over defeats productivity and reusability.

While other implementations to handle event sourcing exist, what is interesting about Emmett is how it provides just enough ceremony to allow us to focus on the application while delivering real value to the developer.

Another powerful aspect is that you still retain a high degree of freedom. You do not have to use the provided CommandHandler if you do not want. In some cases, you may have specific behavior it does not offer. You are free to have your implementation and be happy.

While I haven’t covered this in this article, Emmett already provides a WebApi with ExpressJS support, facilitating you to expose the functionalities via an API.

And did I mention it is still in version 0.5.3? So expect more features to be added.

Have fun!

--

--

Mario Bittencourt
Mario Bittencourt

No responses yet