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. Since each SCA Transaction will not have a large number of 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 of "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
StatefulCalculator
and anEventfulCalculator
. - The
StatefulCalculator
keeps 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
EventfulCalculator
keeps a list of all "events" and folds over them when thetotal
property 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 theStatefulCalculator
instances 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
StatefulCalculator
generates 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))
.