home..

Side Effects ( Storing And Fetching The Data )

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.

domain components

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).

onion architecture image

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:

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.

event-modeling-traditional-systems

The implementation of the state repository is not part of the Application 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.

event-modeling-event-driven-systems

The implementation of the event repository is not part of the Application 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.

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.

event stored vs state stored

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.

aggregates-application

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.

StateStoredAggregate3 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.

EventSourcingAggregate4 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 view5 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:

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!


  1. 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. 

  2. fold is Kotlin extension function public suspend inline fun <T, R> Flow<T>.fold(initial: R, crossinline operation: suspend (acc: R, value: T) -> R): R 

  3. Concrete implementation of the StateStoredAggregate is available here 

  4. Concrete implementation of the EventSourcingAggregate is available here 

  5. Concrete implementation of the MaterializedView is available here 

© 2024 fraktalio d.o.o.