Occurrent – Event Sourcing for the JVM

Introduction

Occurrent is an event sourcing library for the JVM that I have been working on since the summer of 2020. There are several good options for doing event sourcing on the JVM already (such as Axon and Akka) so why did I set out to create something new? This is what we’re going to explore in this article. I intend to write several blog posts on Occurrent in the future with more guides and examples.
We’re going to start off by focusing on the write-side, we’ll look at queries, subscriptions, etc in more detail in a later post.

Contents

  1. Pure Domain Model
  2. Commands
  3. EventStore
  4. Application Service
  5. Conclusion

Philosophy

The philosophy and design decisions of Occurrent is what sets it apart from other options on the JVM. The first, and really crucial point, is the domain model.

Pure Domain Model

A very important design decision in Occurrent is to allow for a domain model without any dependencies to Occurrent or any other library. You are encouraged to express your domain model with pure functions that accept and return domain events. These events represent things of importance that have happened in your domain, for example, that a game was started (implemented by a GameWasStarted event) or that a player guessed the right word (PlayerGuessedTheRightWord event).

There should be no need to extend a framework-specific class such as an AggregateRoot or use framework-specific annotations or event publishers inside your domain model. Your domain functions should not be forced to have side-effects in order to use Occurrent. This doesn’t necessarily mean that you never use any libraries or framework annotations inside your domain model, it just means that you don’t have to! Keeping the model free of infrastructure concerns is a good rule of thumb, and personally, I don’t like frameworks that force me in a different direction when I would argue that there’s a better way. So how would a domain model look like? The structure could look like this:

object WordGuessingGame { 

    fun startNewGame(previousEvents : Sequence<DomainEvent>, gameId : UUID, 
                     startedBy : UUID, secretWord : String) : Sequence<DomainEvent> 
                     = TODO() 
     
    fun guessWord(previousEvents : Sequence<DomainEvent>, playerId : UUID, 
                  guess : String) : Sequence<DomainEvent> = TODO() 
}

If you’re not familiar with Kotlin, you can regard a Sequence as a java.util.Stream (but it’s absolutely fine to use a List instead). Also for non-fictional examples, you probably want to use value objects, or at least type aliases, instead of raw data types such String and UUID. DomainEvent is a data structure (typically a sealed class in Kotlin) that all our domain events will conform to. In Kotlin you do this by letting each domain event extend from DomainEvent, for example:

sealed class DomainEvent {
    abstract val eventId: UUID
    abstract val timestamp: LocalDateTime
    abstract val gameId: UUID
}
data class GameWasStarted(override val eventId: UUID, override val gameId: UUID, 
                          override val timestamp: LocalDateTime,
                          startedBy : UUID, secretWord : String) : DomainEvent()
// More domain events

We will look more closely at how we can implement this domain model in a future blog-post, but for now it’s enough to note that both startNewGame and guessWord are pure! They will not publish any events, call databases or external systems. All they do is to enforce business rules and return new events that represents changes made to a particular game (if any).

Commands

A command is used to represent an intent or decision in an event-sourced system, i.e. something that you want to do. They’re different, in a very important way, from events in that commands can fail or be rejected, where-as events cannot. A typical example of a command would be a data structure whose name is defined as an imperative verb, for example, StartGame. The resulting event, if the command is processed successfully, could then be GameWasStarted.

However, in Occurrent, as explained in more detail below, you are encouraged to start off by not using explicit data structures for commands unless you want to. Occurrent instead promotes (higher-order) pure functions to represent commands (and command handlers), which is exactly what we have in our WordGuessingGame example above.

CloudEvents

So far we’ve briefly discussed domain events. In our example earlier, we represented these events without any infrastructure concerns at all. I.e. in our code-base, we’ve designed our events the way we want them to be represented in our domain model (as sealed classes), without thinking about how they will be represented in an event store. Notice, for example, that we don’t use any (Jackson) annotations, default constructors or other compromises! Again, the main point is not that this is never a valid thing to do, rather that you’re not forced by a framework to do so.

Now, for the first time, we’re going to discuss infrastructure and how to actually store our domain events in an event store. This leads us to another important design decision in Occurrent, the use of CloudEvents.

Cloud events is a CNCF specification for describing event data in a common way. CloudEvents seeks to dramatically simplify event declaration and delivery across services, platforms, and beyond. Occurrent understands CloudEvents and not domain events! In practice, this means that instead of storing events in a proprietary or arbitrary format, Occurrent, stores events in accordance with the cloud event specification, even at the data-store level. I.e. you know the structure of your events, even in the database that the event store uses. This is extremely powerful, not just for peace of mind and data consistency, but it enables features such as (fully-consistent) queries to the event store. We will talk more about this in a subsequent blog post since it’s controversial, but it allows you to derive views from a consistent source, the event store itself.

So, one thing that you need to do when using Occurrent is to map your domain events to and from cloud events. You can decide to do this in a generic fashion or in more fine-grained and optimized way. Here’s a simple example of mapping domain events to a cloud event:

val event : Sequence = .. // New events returned from your domain model

// Map domain events to cloud events
val cloudEvents = events.map { event -> 
    val data = when(event) {
        is GameWasStarted -> """{"secretWord" : "${event.secretWord}", 
                           "startedBy"  : "${event.startedBy.toString()}"}"""
        is PlayerGuessedTheRightWord -> ...
        ...
    }.toByteArray()

    CloudEventBuilder.v1()
        .withId(event.eventId)
        .withSource(URI.create("urn:occurrent:wordguessinggame"))
        .withType(event::class.simpleName!!)
        .withTime(event.timestamp.atOffset(ZoneOffset.UTC))
        .withSubject(event.gameId.toString())
        .withDataContentType("application/json")
        .withData(data)
        .build()
}

You can read-up on the different cloud event properties in the specification.

EventStore

Now that we have converted our domain events to cloud events we’re ready to store them in an event store. Occurrent has support for both reactive non-blocking event stores and normal blocking ones. All implementations currently have support for conditional writes, i.e. you can supply a condition that must be fulfilled in order to actually write the events to the event store. This is crucial to avoid inconsistencies on parellel writes to the same event stream (aggregate). For example:

 
val gameId : UUID = ..
val startedBy : UUID = ..
val secretWord : String = ..

val domainEvents = WordGuessingGame.startNewGame(emptySequence(), gameId, 
                                                 startedBy, secretWord)
val cloudEvents : Sequence = convertToCloudEvents(domainEvents)

eventStore.write(gameId.toString(), 0, cloudEvents)

0 instructs the event store to only store the events if the stream version is equal to 0 (0, in this case, means that no events have been written to the stream). Typically you don’t have to care about this since an application service will figure out the right version to use, regardless if you’re creating a new stream or writing to an existing one.

To read events from the event store you can do like this:

val gameId : UUID = ..
// Read all events from the event store for a particular stream
// The EventStream instance contains the current stream version.
val  eventStream : EventStream = eventStore.read(gameId.toString())

Typically, what you want to do when processing commands is:

  1. Read all events for the stream associated with the aggregate (game in this case)
  2. Invoke a function in the domain model that (may) return new events
  3. Store these new events in the event store

This is exactly what an application service takes care of.

Application Service

An application service is a fancy word for a piece of code that loads events from the event-store and applies them to the domain model, and writes the new events returned from the domain model to the event store as described in the previous section. And the good thing is that it only takes a couple of lines of code to create a generic application service. But fear not, Occurrent already ships with such a generic application service (that you can read more about here). Now that we have all building-blocks and we can start calling functions in our domain model (this is equivalent to dispatching commands in other frameworks). Let’s say we have a REST API in which we receive the game id, secret word, and the id of the player that starts the new game:

// Now in your REST API use the application service function:
val gameId = ... // From a form parameter
val startedBy = ... // From a form parameter
val wordToGuess = .. // From a form parameter

applicationService.execute(gameId) { events -> 
    WordGuessingGame.startNewGame(events, gameId, startedBy, wordToGuess)
}

That’s it, no need for explicit data structures for your commands! You can use the same application service when a player guesses a word:

// Now in your REST API use the application service function:
val gameId = ... // From a form parameter
val playerId = ... // From a form parameter
val guess = .. // From a form parameter

applicationService.execute(gameId) { events -> 
    WordGuessingGame.guessWord(events, gameId, playerId, guess)
}

Occurrent also has support for composing several “commands” into one without changing your domain model. For example, if you want to start a new game and make a guess in the same transaction. Refer to the documentation for more info on how to achieve this. It’s also possible to execute so-called policies in the same transaction as your domain logic. This can be really useful if you just want to create a simple synchronous UI. Again, refer to the documentation for more details.

Conclusion

As you’ve hopefully seen, Occurrent allows you to create a domain model using pure functions, without any dependencies to Occurrent what so ever. You are in charge of your code! Again, it could be perfectly fine to use libraries and even framework annotations in your domain model, but it’s your choice.

What Occurrent aims to do, is to make use of some fundamental properties of functional programming on the write side, to allow solving common problems with less framework support. It also aims to be simple (in the Rich Hickey sense of the word), composability, and pragmatic (we’ll look into this more in a future blog post).

Having distinct small components allows you to tailor them for the needs your specific application. It allows you to make trade-offs and sometimes even taking some shortcuts (such as updating a read model in the same transaction as your writes), as long as you do it in a responsible way (such as deriving your new read model from the events returned by the domain model).

It would be great if you want to try out Occurrent and provide feedback. Occurrent is completely open-source and you’re very welcome to get involved in any shape or form. Please visit the website for more information.

Leave a Reply

Your email address will not be published. Required fields are marked *