Skip to main content

A Case Study: Simple Event Sourcing

Posted by manning_pubs on March 25, 2013 at 7:06 AM PDT



A Case Study: Simple Event Sourcing

by Michael Fogus and Chris Housen, author of The Joy of Clojure, Second Edition

Event sourcing is an interesting architecture model that defines system state solely in terms of system events. In this article, based on chapter 14 of The Joy of Clojure, Second Edition, the authors lead you through a case study that dives into event sourcing.

Imagine a system that models a baseball player’s statistical achievements. One way to model such a system is to store a table of the player’s statistics like the times he’s batted and the number of hits.

Table 1 A baseball rectangle

At bats Hits Average
5 2 0.400

However, an equally good model, and one that has interesting properties, is an event-sourced model. In an event-sourced model, the state is derived from a sequence (a strict sequence is not always required) of events pertaining to the domain in question. In the case of our baseball player, an example is shown in table 2.

Table 2 Baseball events

Event number Result
1 :out
2 :hit
3 :out
4 :hit
5 :out

At the end of the events, the state of the player in question is exactly the same, 5 at bats with 2 hits, but, with event sourcing, it’s derived from the events themselves.

Events as data

Before we explore the implementation of a simple event-sourcing engine, we’ll take a moment to make a point. Recent advances in system design have taken advantage of a client server model exchanging data in a regular format (for examples, JSON). However, we can extend this idea further by exchanging event data between disparate, polyglot systems (written in different programming languages). While the codebases can take any form in such systems, their behaviors are driven via events toward a common goal.

Systems built in this way are made possible by observing that the events comprising the unit of currency in an event-sourced model are themselves data! This is a powerful idea (didn’t you notice the exclamation mark?) because if our events are data, then they can be persisted in a database and serve in a straightforward way to recreate a system’s state at any time in the future. This is the model that will sketch herein.

A simple event sourced model

Our event sourced model starts with a snapshot of some state, in the case of the baseball world it would simply be a map like the following:

{:ab 5
:h 2
:avg 0.400}

This is very similar to the rectangle model except that this state is never directly modified, but instead derived from events, which themselves are maps:

{:result :hit}

To get from an event, or a sequence of events requires a few auxiliary functions. The first function is a function called valid? that just checks the form of an event, we’ll keep it simple as shown below:

(ns joy.event-sourcing)

(defn valid? [event]
  (boolean (:result event)))

(valid? {})
;=> false

A second function required is an effect function that takes a state and event and applies the event to the state in the proper way, as shown in listing 1.

Listing 1 A simplified event sourcing state effecting function

(defn effect [{:keys [ab h] :or {ab 0, h 0}}
              event]
  (let [ab (inc ab)
        h (if (= :hit (:result event))
            (inc h)
            h)
        avg (double (/ h ab))]
    {:ab ab :h h :avg avg}))

Running effect through a few tests, validates our thinking:

(effect {} {:result :hit})
;=> {:ab 1 :h 1 :avg 1.0}

(effect {:ab 599 :h 180}
        {:result :out})

;=> {:ab 600 :h 180 :avg 0.3}

It would be nice to use the valid? function before applying an event to our state, so we’ll define a new function that uses it in listing 2 next.

Listing 2 A function that only applies an effect when the event is valid

(defn apply-effect [state event]
  (if (valid? event)
    (effect state event)
    state))

Finally, we can define another function effect-all that takes a state, and a sequence of events and returns a final state as shown in listing 3 below.

Listing 3 A simplified event sourcing mass effecting function

(def effect-all #(reduce apply-effect %1 %2))

Taking effect-all for a spin in listing 4 proves our model.

Listing 4 A simplified event sourcing mass effecting function

(def events (repeatedly 100
              (fn []
                (rand-map 1
                  #(-> :result)
                  #(if (even? (rand-int 2))
                     :hit
                     :out)))))
(effect-all {} events)
;=> {:ab 100 :h 52 :avg 0.52}

What we have at the end of the call to is a snapshot effect-all of the state at the time of the 100th event. To rewind is as simple as applying only a subset of the events, as shown below:

(effect-all {} (take 50 events))
    ;=> {:ab 50 :h 24 :avg 0.48}

The events can be sliced and diced in any conceivable way to information about the states at any given moment along the event stream. Further, we can change the way that states are saved to gather a historical timeline, as shown below:

(def fx-timeline #(reductions apply-effect %1 %2))

(fx-timeline {} (take 3 events))
;=> ({}
;    {:ab 1, :h 0, :avg 0.0}
;    {:ab 2, :h 0, :avg 0.0}
;    {:ab 3, :h 1, :avg 0.3333333})

We could use fx-timeline to infer trends, build histograms, and many other useful actions. With an event-sourced model, the possibilities are seemingly endless.

Essential state and derived state

One particularly sharp (as the double-edged sword goes) aspect of the event-sourcing model is the differentiation between essential and derived state. In our baseball model the essential state is the state that is created from direct mappings in the event model, namely (from the event count) :ab and :h (from the :result type). However, the derived state is the data that exists only in code, derived from specific rules and conditions therein. In our baseball model the :avg state element is derived from the mathematical relation between two essential elements.

By virtue of storing the events, we can quite easily make changes to live systems. That is, if a system’s state is the product of the event stream, then we should be able to recreate it at any time by re-running the events into a fresh system (martinfowler.com 2011). However, if the code generating the derived state changes in the interim, then re-running the events may not result in the state expected. Using a language like Clojure that is built with data processing in mind helps to soften the pain of deep code changes affecting the generation of derived state.

Summary

By implementing an event-sourcing model, we’ve applied the data-programmable engine paradigm to state calculation itself. This is a powerful approach to constructing software, the benefits of which we’ve only scratched the surface to.


Here are some other Manning titles you might be interested in:

The Real-World Functional Programming

The Real-World Functional Programming
Tomas Petricek with Jon Skeet

Scala

Scala in Action
Nilanjan Raychaudhuri

Play for Scala

IronPython in Action
Peter Hilton, Erik Bakker, and Francisco Canedo


AttachmentSize
jocl2001.png4.12 KB
jocl2002.png4.33 KB
jocl2003.png4.4 KB
Related Topics >>