This sample code is meant to gently introduce the concept of event sourcing at the level of concrete implementation, using Kotlin. For conceptual background, I recommend starting with:
- https://martinfowler.com/eaaDev/EventSourcing.html -- 20m
- https://www.kurrent.io/event-sourcing -- 30m
- Event Sourcing 101 Series by Anna McDonald of Confluent -- 25m
- Ch7 "Modeling the Dimension of Time" from Learning Domain Driven Design by Vlad Khononov -- 1h
To see the code directly in action:
- Simple version -- Expected results:
statefulCalc total: 5 eventfulCalc1 total: 5 eventfulCalc2 total: 5 eventCalc1 events: [Added(1), Subtracted(1), Added(1), Added(1), Added(1), Multiplied(2), Subtracted(1)] eventCalc2 events: [Added(1), Multiplied(4), Added(1)] - Advanced version with temporal/historical state reconstituion capability -- Expected results:
eventfulCalc1 total: 5 eventfulCalc1 events: [ Added(1@11:11:31:285), Subtracted(1@11:11:31:407), Added(1@11:11:31:471), Added(1@11:11:31:519), Added(1@11:11:31:620), Multiplied(2@11:11:31:754), Subtracted(1@11:11:31:866) ] eventulCalc1 history: eventfulCalc1 total as of 1: 1 eventfulCalc1 total as of 2: 0 eventfulCalc1 total as of 3: 1 eventfulCalc1 total as of 4: 2 eventfulCalc1 total as of 5: 3 eventfulCalc1 total as of 6: 6 eventfulCalc1 total as of 7: 5
When storing events to track the complete path of state transitions for an aggregate, it is always possible to also store a projection of the current state, but this is usually only necessary for aggregates that have large numbers of events. For the lifecycle of an independent aggregate that will have tens or even hundreds of small events, that is probably overkill.
Instead, a simple process of reading the list of past events and flowing them through a function that is capable of producing, or projecting, a model is rather straight forward. The most important part is to design this such that it only operates on the events, not on the original requests, or Commands, since these often have side-effects like calling APIs, saving data, etc.
In Kotlin, this is easily achieved with a left fold over the set of events. For illustration purposes, consider the following simple illustration. You can run and edit the sample online here in the Kotlin Playground.
class StatefulCalculator()
{
var total = 0
fun Add(value : Int = 1) : StatefulCalculator {
total = total + value
return this
}
fun Subtract(value : Int = 1) : StatefulCalculator {
total = total - value
return this
}
fun Multiply(value : Int = 2) : StatefulCalculator {
total = total * value
return this
}
}
open class Operation(val value : Int = 1) {
override fun toString() : String = "${this.javaClass.name}(${value})"
}
class Added(value : Int = 1) : Operation(value)
class Subtracted(value : Int = 1) : Operation(value)
class Multiplied(value : Int = 2) : Operation(value)
class EventfulCalculator() {
private var events = mutableListOf<Operation>()
fun Add(value : Int = 1) : EventfulCalculator {
events.add(Added(value))
return this
}
fun Subtract(value : Int = 1) : EventfulCalculator {
events.add(Subtracted(value))
return this
}
fun Multiply(value : Int = 2) : EventfulCalculator {
events.add(Multiplied(value))
return this
}
val total get() = events.fold(0) { total, item ->
when(item) {
is Added -> total + item.value
is Subtracted -> total - item.value
is Multiplied -> total * item.value
else -> total
}
}
val eventsList get() = events
}
fun main() {
val statefulCalc = StatefulCalculator()
statefulCalc
.Add()
.Subtract()
.Add()
.Add()
.Add()
.Multiply()
.Subtract()
println("statefulCalc total: ${statefulCalc.total}")
val eventfulCalc1 = EventfulCalculator()
eventfulCalc1
.Add()
.Subtract()
.Add()
.Add()
.Add()
.Multiply()
.Subtract()
println("eventfulCalc1 total: ${eventfulCalc1.total}")
val eventfulCalc2 = EventfulCalculator()
eventfulCalc2
.Add()
.Multiply(4)
.Add()
println("eventfulCalc2 total: ${eventfulCalc2.total}")
println("eventCalc1 events: ${eventfulCalc1.eventsList}")
println("eventCalc2 events: ${eventfulCalc2.eventsList}")
}statefulCalc total: 5
eventfulCalc1 total: 5
eventfulCalc2 total: 5
eventCalc1 events: [Added(1), Subtracted(1), Added(1), Added(1), Added(1), Multiplied(2), Subtracted(1)]
eventCalc2 events: [Added(1), Multiplied(4), Added(1)]
- In this code, we have both a
StatefulCalculatorand anEventfulCalculator. - The
StatefulCalculatorkeeps a simple integer value and mutates it with every method call, which results in a loss of information about how the value actually ended up the way it is. - The
EventfulCalculatorkeeps a list of all "events" and folds over them when thetotalproperty is accessed to produce the "current state".- Important: events are all named in the past tense because these are facts about things that took place.
- The public calls to each type of calculator are the same.
- Note that even though all instances of the calculators produce the value
5, only theStatefulCalculatorinstances are capable of showing us the radically different way that the value was produced.
Obviously, events in the SCA domain will be much more information-rich than in this simple example, but the concepts are identical for applying events to produce current state or any other imagined read model.
As a quick demonstration of a more advanced StatefulCalculator, let's get rid of the other one and add a createDate field along with a way to show both a timeline of the events and to query for that the total was at any step along the way to the completed total. This version also introduces a bit more OOP by getting rid of the when statement in favor of the Operation class having an abstract operate method.:
import java.util.Date
import java.text.SimpleDateFormat
import kotlin.math.roundToInt
abstract class Operation(val value : Int = 1, val createDate : Date = Date()) {
override fun toString() : String = "${this.javaClass.name}(${value}@${formatter.format(createDate)})"
abstract fun operate(currentTotal: Int) : Int
private val formatter = SimpleDateFormat("HH:mm:ss:SSS")
}
class Added(value : Int = 1) : Operation(value) {
override fun operate(currentTotal: Int) = currentTotal + value
}
class Subtracted(value : Int = 1) : Operation(value) {
override fun operate(currentTotal: Int) = currentTotal - value
}
class Multiplied(value : Int = 2) : Operation(value) {
override fun operate(currentTotal: Int) = currentTotal * value
}
class EventfulCalculator() {
private var events = mutableListOf<Operation>()
fun Add(value : Int = 1) : EventfulCalculator {
delay()
events.add(Added(value))
return this
}
fun Subtract(value : Int = 1) : EventfulCalculator {
delay()
events.add(Subtracted(value))
return this
}
fun Multiply(value : Int = 2) : EventfulCalculator {
delay()
events.add(Multiplied(value))
return this
}
val total get() = calculateTotal(events)
fun totalAsOf(operationNumber : Int = 1) = calculateTotal(events.take(operationNumber))
private fun calculateTotal(ops : List<Operation>) = ops.fold(0) {
currentTotal, item -> item.operate(currentTotal)
}
val eventsList get() = events
private fun delay() = Thread.sleep((Math.random() * 250).roundToInt().toLong())
}
fun main() {
val eventfulCalc1 = EventfulCalculator()
eventfulCalc1
.Add()
.Subtract()
.Add()
.Add()
.Add()
.Multiply()
.Subtract()
println("eventfulCalc1 total: ${eventfulCalc1.total}")
println("eventfulCalc1 events: ${eventfulCalc1.eventsList}")
println("eventulCalc1 history:")
println("eventfulCalc1 total as of 1: ${eventfulCalc1.totalAsOf(1)}")
println("eventfulCalc1 total as of 2: ${eventfulCalc1.totalAsOf(2)}")
println("eventfulCalc1 total as of 3: ${eventfulCalc1.totalAsOf(3)}")
println("eventfulCalc1 total as of 4: ${eventfulCalc1.totalAsOf(4)}")
println("eventfulCalc1 total as of 5: ${eventfulCalc1.totalAsOf(5)}")
println("eventfulCalc1 total as of 6: ${eventfulCalc1.totalAsOf(6)}")
println("eventfulCalc1 total as of 7: ${eventfulCalc1.totalAsOf(7)}")
}eventfulCalc1 total: 5
eventfulCalc1 events: [
Added(1@11:11:31:285),
Subtracted(1@11:11:31:407),
Added(1@11:11:31:471),
Added(1@11:11:31:519),
Added(1@11:11:31:620),
Multiplied(2@11:11:31:754),
Subtracted(1@11:11:31:866)
]
eventulCalc1 history:
eventfulCalc1 total as of 1: 1
eventfulCalc1 total as of 2: 0
eventfulCalc1 total as of 3: 1
eventfulCalc1 total as of 4: 2
eventfulCalc1 total as of 5: 3
eventfulCalc1 total as of 6: 6
eventfulCalc1 total as of 7: 5
- In this version, the
StatefulCalculatorgenerates a timestamp every time it stores an event. It also adds an artificial delay to simulate real-world user interactions which have natural variations. - The output shows us the complete series of events along with the timestamp.
- And, it shows us the total at any step by virtue of the new and simple method
fun totalAsOf(operationNumber : Int = 1) = calculateTotal(events.take(operationNumber)).