Some weeks ago I published a demo project on GitHub showing a Domain Driven Design with Kotlin. You can find the original blog post right here. Now I took the approach one step further and added Event Sourcing to the demo. Here’s what I did.
Event Sourcing
The basic idea of Event Sourcing is to store a stream of events instead the current state of an object. So Event Sourcing is a different persistence approach.
Traditionally, we would store the current state of an object in a relational database. We would map the object to tables and columns and update them every time the object changes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Product { val id = 42 fun update(...) { this.name = “Coca-Cola” this.price = 1.99 } } repository.save(product) ID | Name | Price ---|------------|---------- 42 | Coca-Cola | 1.99 43 | Water | 0.99 |
Event Sourcing takes a different approach. Instead of saving the object, all changes to the object are saved. In Domain Driven Design and Event Sourcing those changes are called events. The sum of all events make up the current state of the object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Product { val id = 42 fun update(...) { ... return ProductUpdatedEvent(42, “Coca-Cola”, 1.99) } } eventStore.save(event) ID | Type | Data ---|---------------------|----------------------------- 42 | ProductCreatedEvent | { 42, Cola, null } 42 | PriceUpdatedEvent | { 42, 0.00 } 42 | ProductUpdatedEvent | { 42, Coca-Cola, 1.99 } 43 | ProductUpdatedEvent | { 43, Water, 0.99 } |
The benefit of this type of persistence is that we always keep the whole history of an object. We can see each and every change which occurred over time.
The downside is a more complicated and unfamiliar programming concept:
To implement Event Sourcing all changes must be published via events. Even the smallest change to our domain model (setting some flag from true
to false
) must result in an event. Any data which is not part of an event will be lost.
Having those events, we must implement two functionalities: (1) First we need a business method which takes an input, does things (calculations, calls, conversions, etc.) and finally throws an event with all data involved. (2) Second we need another method which takes the event and simply applies it to the model.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Product { fun update(...) { // Business logic with calculations, calls, etc... return ProductUpdatedEvent(...) } fun apply(ProductUpdatedEvent) { // Apply data! Don’t do anything else! this.name = event.name this price = event.price } } |
The first method performs business logic and actually does something whereas the second function is just “a setter for data”. We need this differentiation, because whenever we load an object all events from the database must be applied to it. This must be done without triggering business logic and side effects.
1 2 |
val events = eventStore.findAllById(42) val product = Product().applyAll(events) |
Demo on GitHub
As this description might sound a little bit weird, I’ve prepared a small demo project on GitHub:
The demo shows a small use-case and provides a web UI which helps to see what is happening behind the scenes:
Presentation
Best regards,
Thomas
Thanks it is very nice idea to keep track of changes.
Thank you for this nice demo and blog – very useful 🙂
But some load leads to concurrency problems:
2019-01-24 20:22:50.489 ERROR 1340 — [kExecutor-75460] .a.i.SimpleAsyncUncaughtExceptionHandler : Unexpected error occurred invoking async method ‘public void de.bringmeister.connect.product.application.product.ProductService.handle(de.bringmeister.connect.product.domain.product.UpdateMasterDataCommand)’.
java.util.ConcurrentModificationException: null
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:907) ~[na:1.8.0_151]
at java.util.ArrayList$Itr.next(ArrayList.java:857) ~[na:1.8.0_151]
at de.bringmeister.connect.product.infrastructure.stubs.StubbedEventStore.allFor(StubbedEventStore.kt:32) ~[main/:na]
at de.bringmeister.connect.product.application.product.ProductService.handle(ProductService.kt:33) ~[main/:na]
at de.bringmeister.connect.product.application.product.ProductService$$FastClassBySpringCGLIB$$efff18da.invoke() ~[main/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:746) ~[spring-aop-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115) ~[spring-aop-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_151]
at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_151]