Event sourcing in Clojure.
Recently I came across a blog post by James Nugent that described some example patterns for implementing event sourcing in Go. I'm no expert, I've not studied much Go code before and I've never written any but I'd certainly have to agree with the observation James made, there is much less code than some C# implementations.
James made available the code for his post on GitHub, so I thought it would be interesting to have a look and come up with a Clojure implementation based on the same domain. I'd suggest reading James post as this one will follow the same structure.
FWIW, the comparison between Go and Clojure is a bit of an odd one really, this is just a bit of fun.
Events
As per James's post we will use a simplified domain model of a frequent flier. James uses mutable Go structures to define his events, we can make use of immutable Clojure records. We could probably just use Clojure maps but I'm planning to perform dispatch based on the event type, so a record or type is probably more suitable.
If we really wanted to use maps then we could probably attach some meta data to them using Clojures with-meta and then switch on that meta data. I don't think we need to worry about it though, Clojure records can be used as if they were maps in most scenarios.
Go
Clojure
Obviously the Clojure version of the events are dynamically typed, we could use type hints if we really wanted to.
Aggregates
In the Go example, James uses structs to represent aggregates and capture the state that will enable invariants during future behaviour, e.g. changing the frequent fliers status based on the stateful "tier points" value.
Go
Clojure
Its worth noting that the Go code above uses a Status
enumeration to represent the frequent flier status, in the Clojure example I'll just be using the keys :red
, :silver
and :gold
.
State transition and change tracking
Go
Clojure
In general the Clojure version of the code implements the same pattern as the Go code. We call a function on the aggregate, perform some invariant checking if applicable, track the event and apply the "state" transition.
The Go version of the code switches based on the type of the event being processed and applies the state mutations.
The Clojure version of the code uses a multimethod to dispatch based on the event type being processed, each chunk of "state" mutation is isolated in its own function. Of course, when we say mutation that's not true, since really we just return a new version of the aggregate with the updated "state".
The Clojure code benefits from having no mutable state, each of the functions are pure in nature. James does mention in his post that having immutable records can make for a more elegant implementation. I'm not sure the Clojure version is "more elegant" but I think its pretty nice.
Loading history
Go
Clojure
In the situation where you need to hydrate an aggregate using a collection of past events I think the Clojure implementation is pretty neat. All we need to do is (reduce transition ff history)
. We reduce the history of events using the transition function and an inital default state frequent flier. If we had a massive number of events we could probably even use Clojures fold reducer, this would carry out the reduce operation in parallel. We also assoc in an expected version based on the history.
Example usage
Go
Clojure
Both of the above examples hydrate a frequent flier from a history of events and print out the aggregate. They then record another FlightTaken
event so that the accumulation of tier points causes an upgrade to gold status.
Conclusion
Have a look at both the Go version and the Clojure version and see what you think. I noticed a few things:- Lisp is beautiful (subjective at best)
- Clojures immutability and built in support for things like map/reduce make things like hydrating an aggregate easy
- Multimethods are quite a consice way of dispatching based on an event type
- Go is a bit tricky to read but then again I am a total newbie
If you know Clojure then please let me know if I'm doing anything really stupid or not very idiomatic by sending a pull request.
I'm going to write a Clojure follow up post with a more complicated domain model and a storage implementation, probably using Event Store.