Side Effects ( Storing And Fetching The Data )
Written by Ivan Dugalic - November 2021
In the previous blog post, we learned how to model the domain effectively by using types and functions.
We used decider
, view
, and saga
components from FModel1 library to model the behavior using pure functions.
- These components do not care about storing or fetching the data.
- They do not produce any side effects of this kind.
- They represent pure computation / pure logics of a program (algebras).
Functional Programming emphasizes separating the pure logic of a program (algebras) and the runtime used to run it.
Flagging a computation as
suspend
enforces a calling context, meaning the compiler can ensure that we can’t call the effect from anywhere other than an environment prepared to run suspended effects. That will be another suspended function or a Coroutine. This effectively means we’re decoupling the pure declaration of our program logic (frequently called algebras in the functional world) from the runtime. And therefore, the runtime has the chance to see the big picture of our program and decide how to run and optimize it.
The Application layer
The logic execution will be orchestrated by the outside components that use the domain components (decider
, view
, and saga
) to do the computations. These components will be responsible for fetching and saving the data (suspended effects).
The arrows in the image show the direction of the dependency. Notice that all dependencies point inwards and that Domain does not depend on anybody or anything.
Pushing these decisions from the core domain model is very valuable. Being able to postpone them is a sign of good architecture.
Event-stored or State-stored
The domain model is explicitly modeling events
and state,
and this opens some interesting options:
- fetch
state
, execute domain components/compute, and store newstate
by overwriting the previous state in the storage / traditional / state-stored systems - fetch
events
, execute domain components/compute, and store newevents
by appending to the previous events in the storage / event-driven systems / event-stored systems - or combine these two options: fetch
events
andstate
, execute domain components/compute, and store newevents
andstate
.
You can compose functions from the domain decider
component in order to implement any of these options:
State-stored
val state = fetchState(command)
val events = decider.decide(command, state)
val newState = events.fold(state, decider.evolve)
save(newState)
A state-stored system is fetching the current state by using the fetchState
method from the state repository
.
It then delegates the command and the current state to the decider
which will produce a new list of events by executing the decide
function.
Finally, the list of events will fold2 to the new state by using the decider
evolve
function, and the new state will be stored via the save
function from the state repository
.
The implementation of the
state repository
is not part of theApplication
layer. It is delegated to the outside Infrastructure/Adapter layers.
Event-stored / Event-sourced
val events = fetchEvents(command)
val state = events.fold(decider.initialState, decider.evolve)
var newEvents = decider.decide(command, state)
save(newEvents)
An event-stored system is fetching the events by using the fetchEvents
method from the event repository
.
It then evolves the state based on the fetched events and delegates the state and the command to the decider
component which will produce new events by executing the decide
function.
Finally, the events will be stored via the save
function from the event repository
.
The implementation of the
event repository
is not part of theApplication
layer. It is delegated to the outside Infrastructure/Adapter layers.
Event Sourcing pattern
Event Sourcing pattern mandates that the state change of the system isn’t explicitly stored in the database as the new state (overwriting the previous state) but as a sequence of decisions/facts/events (by appending). This way, you don’t lose any data/information, and you make the next decisions/facts/events based on the previous events.
- You write down the decisions/facts/events in sequence (gives you
what
) /save(events)
- Every new decision is based on the previous decisions (
events=fetchEvents(command)
) and the current intent/command (gives youwhy
) /decider.decide(command, events.fold(decider.initialState, decider.evolve))
- Obviously, you have the notion of time (gives you
when
) / events are arranged on the time-line in sequence - Feel free to enrich with
who
made these decisions / events hold the authority or the role
It is much more than a log or audit trail. A simple log is storing what
happened, but the next decision/event is not made based on the log entries, and you lack why
in this case.
There is more to it! You can switch from one system type to another or have both flavors included within your systems landscape.
Aggregates
There are many possible implementations of this layer. We are going to show you one possibility that is using a tactical pattern of Aggregates from Domain-Driven Design in combination with Event Sourcing and CQRS patterns.
An Aggregate is an entity or group of entities always kept in a consistent state (within a single ACID transaction). The Aggregate Root is the entity within the aggregate that is responsible for maintaining this consistent state.
State-stored Aggregate
State-stored Aggregate is a formalization of the state-stored system mentioned previously.
State-stored Aggregate is using/delegating a Decider
to handle commands and produce new state. It belongs to the Application layer. In order to
handle the command, aggregate needs to fetch the current state via StateRepository.fetchState
function first, and then
delegate the command to the decider which can produce new state as a result. New state is then stored
via StateRepository.save
suspending function.
StateStoredAggregate
3 extends IDecider
(decision-making) and StateRepository
(fetch-save) interfaces, clearly communicating that it is composed
out of these two behaviours.
interface StateStoredAggregate<C, S, E> : IDecider<C, S, E>, StateRepository<C, S>
The Delegation pattern has proven to be a good alternative to implementation inheritance
, and Kotlin supports it
natively requiring zero boilerplate code. stateStoredAggregate
function is a good example:
fun <C, S, E> stateStoredAggregate(
decider: IDecider<C, S, E>,
stateRepository: StateRepository<C, S>
): StateStoredAggregate<C, S, E> =
object :
StateStoredAggregate<C, S, E>,
StateRepository<C, S> by stateRepository,
IDecider<C, S, E> by decider {}
Event-sourcing Aggregate and Views
Event-sourcing Aggregate is a formalization of the event-stored/event-sourced system mentioned previously.
Event-sourcing Aggregate is using/delegating a Decider
to handle commands and produce events. It belongs to the Application layer. In order to
handle the command, aggregate needs to fetch the current state (represented as a list of events)
via EventRepository.fetchEvents
function, and then delegate the command to the decider which can produce new events as
a result. Produced events are then stored via EventRepository.save
suspending function.
EventSourcingAggregate
4 extends IDecider
(decision-making) and EventRepository
(fetch-save) interfaces, clearly communicating that it is composed
out of these two behaviours.
interface EventSourcingAggregate<C, S, E> : IDecider<C, S, E>, EventRepository<C, E>
The Delegation pattern has proven to be a good alternative to implementation inheritance
, and Kotlin supports it
natively requiring zero boilerplate code.
eventSourcingAggregate
function is a good example:
fun <C, S, E> eventSourcingAggregate(
decider: IDecider<C, S, E>,
eventRepository: EventRepository<C, E>
): EventSourcingAggregate<C, S, E> =
object :
EventSourcingAggregate<C, S, E>,
EventRepository<C, E> by eventRepository,
IDecider<C, S, E> by decider {}
Example:
/**
* A convenient type alias for Decider<RestaurantOrderCommand?, RestaurantOrder?, RestaurantOrderEvent?>
*/
typealias RestaurantOrderDecider = Decider<RestaurantOrderCommand?, RestaurantOrder?, RestaurantOrderEvent?>
/**
* A convenient type alias for EventRepository<RestaurantOrderCommand?, RestaurantOrderEvent?>
*/
typealias RestaurantOrderAggregateEventStoreRepository = EventRepository<RestaurantOrderCommand?, RestaurantOrderEvent?>
val aggregate = eventSourcingAggregate(restaurantOrderDecider, restaurantOrderAggregateEventStoreRepository)
aggregate.handle(restaurantOrderCommand)
The RestaurantOrderDecider
component belongs to the core Domain layer. It represents the main decision-making algorithm for the Restaurant Order process, and it was explained in the previous blog post.
All you need to do is to implement EventRepository<RestaurantOrderCommand?, RestaurantOrderEvent?>
. Check out the source code on Github
CQRS unlocks the Event Sourcing pattern by effectively splitting the domain model into a command model and query model. The view model is continuously updated to contain a certain representation of the current state, based on the events published by the command model.
Materialized view is using/delegating a View
(domain component) to handle events of type E
and to maintain a state of denormalized projection(s) as a result.
Essentially, it represents the query/view side/model of the CQRS pattern. The Event-sourcing Aggregate represents the command side/model of the CQRS pattern.
Materialized view
5 extends IView
(decision-making) and ViewStateRepository
(fetch-save) interfaces, clearly communicating that it is composed
out of these two behaviours.
interface MaterializedView<S, E> : IView<S, E>, ViewStateRepository<E, S>
// Notice the `delegation pattern`
fun <S, E> materializedView(
view: IView<S, E>,
viewStateRepository: ViewStateRepository<E, S>,
): MaterializedView<S, E> =
object : MaterializedView<S, E>, ViewStateRepository<E, S> by viewStateRepository, IView<S, E> by view {}
Please note that
StateStoredAggregate
models the command and the query side model altogether. It is not utilizing Event Sourcing or CQRS patterns.
Example:
/**
* A convenient type alias for View<RestaurantOrderViewState?, RestaurantOrderEvent?>
*/
typealias RestaurantOrderView = View<RestaurantOrderViewState?, RestaurantOrderEvent?>
/**
* A convenient type alias for ViewStateRepository<RestaurantOrderEvent?, RestaurantOrderViewState?>
*/
typealias RestaurantOrderMaterializedViewStateRepository = ViewStateRepository<RestaurantOrderEvent?, RestaurantOrderViewState?>
val materializedView = materializedView(restaurantOrderView, restaurantOrderMaterializedViewStateRepository)
materializedView.handle(restaurantOrderEvent)
All you need to do is to implement ViewStateRepository<RestaurantOrderEvent?, RestaurantOrderViewState?>
. Check out the source code on Github
We pushed side effects from our core domain to the edges of the system, enabling flexibility and allowing you to choose (switch to) the frameworks/libraries you like without changing your core domain logic at all:
- Spring
- Micronaut
- Ktor
- JPA
- JDBC
- AxonFramework
With this design, you can smoothly transit from traditional systems (state-stored) to event-driven systems (event-sourced as well) or the other way around. These two patterns (event sourcing, CQRS) shape the modern, cost-effective, maintainable, and scalable software design.
In the following blog post, we will discuss how we can effectively handle Errors (side effects) by classifying them as Business Errors/Events, Technical Errors, and Bugs.
The source code of the demo application is publicly available here.
Happy coding!
-
FModel can be used as a library, or as an inspiration, or both. It provides just enough tactical Domain-Driven Design patterns, optimised for Event Sourcing and CQRS. ↩
-
fold
is Kotlin extension functionpublic suspend inline fun <T, R> Flow<T>.fold(initial: R, crossinline operation: suspend (acc: R, value: T) -> R): R
↩ -
Concrete implementation of the
StateStoredAggregate
is available here ↩ -
Concrete implementation of the
EventSourcingAggregate
is available here ↩ -
Concrete implementation of the
MaterializedView
is available here ↩