f(
model)
- Functional and Reactive Domain Modeling
When you’re developing an information system to automate the activities of the business, you are modeling the business.
The abstractions that you design, the behaviors that you implement, and the UI interactions that you build all reflect
the business — together, they constitute the model of the domain.
IOR<Library, Inspiration>
This project can be used as a multiplatform library, or as an inspiration, or both. It provides just enough tactical
Domain-Driven Design patterns, optimised for Event Sourcing and CQRS.
- The
domain
model library is fully isolated from the application layer and API-related concerns. It represents a pure
declaration of the program logic. It is written in Kotlin programming language, without
additional
dependencies. - The
application
libraries orchestrates the execution of the logic by loading state, executing domain
components
and storing new state. It is written in Kotlin programming language. Two flavors (
extensions of Application
module) are available:
application-vanilla
is using plain/vanilla Kotlin to implement the application layer in order to load the state,
orchestrate the execution of the logic and save new state.application-arrow
is using Arrow and Kotlin to implement the application layer in order
to load the state, orchestrate the execution of the logic and save new state - managing errors much better (using
Either).
The libraries are non-intrusive, and you can select any flavor, or choose both (vanila
and arrow
). You can use
only domain
library and model the orchestration (application
library) on your own. Or, you can simply be inspired by
this project :)
Table of Contents
Multiplatform
Support for multiplatform programming is one of Kotlin’s key benefits. It reduces time spent writing and maintaining the
same code for different platforms while retaining the flexibility and benefits of native programming.
Abstraction and generalization
Abstractions can hide irrelevant details and use names to reference objects. It emphasizes what an object is or does
rather than how it is represented or how it works.
Generalization reduces complexity by replacing multiple entities which perform similar functions with a single
construct.
Abstraction and generalization are often used together. Abstracts are generalized through parameterization to provide
more excellent utility.
decide: (C, S) -> Flow<E>
On a higher level of abstraction, any information system is responsible for handling the intent (Command
) and based on
the current State
, produce new facts (Events
):
- given the current
State/S
on the input, - when
Command/C
is handled on the input, - expect
flow
of new Events/E
to be published/emitted on the output
evolve: (S, E) -> S
The new state is always evolved out of the current state S
and the current event E
:
- given the current
State/S
on the input, - when
Event/E
is handled on the input, - expect new
State/S
to be published on the output
Event-sourced or State-stored systems
- State-stored systems are traditional systems that are only storing the current State by overwriting the previous State
in the storage.
- Event-sourced systems are storing the events in immutable storage by only appending.
A statement:
Both types of systems can be designed by using only these two functions and three generic parameters:
decide: (C, S) -> Flow<E>
evolve: (S, E) -> S
There is more to it! You can switch from one system type to another or have both flavors included within your systems
landscape.
A proof
We can fold/recreate the new state out of the flow of events by using evolve
function (S, E) -> S
and providing the
initialState of type S as a starting point.
Flow<E>.fold(initialState: S, ((S, E) -> S)): S
Essentially, this fold
is a function that is mapping a flow of Events to the State:
We can now use this function (Flow<E>) -> S
to:
- contra-map our
decide
function ((C, S) -> Flow<E>
) over S
type to: (C, Flow<E>) -> Flow<E>
- this is an
event-sourced system - or to map our
decide
function ((C, S) -> Flow<E>
) over E
type to: (C, S) -> S
- this is a state-stored
system
Two functions are wrapped in a datatype class (algebraic data structure), which is generalized with three generic
parameters:
data class Decider<C, S, E>(
val decide: (C, S) -> Flow<E>,
val evolve: (S, E) -> S,
)
Decider
is the most important datatype, but it is not the only one. There are others:
Decider
Decider
is a datatype that represents the main decision-making algorithm. It belongs to the Domain layer. It has three
generic parameters C
, S
, E
, representing the type of the values that Decider
may contain or use.
Decider
can be specialized for any type C
or S
or E
because these types do not affect its
behavior. Decider
behaves the same for C
=Int
or C
=YourCustomType
, for example.
Decider
is a pure domain component.
C
- CommandS
- StateE
- Event
data class Decider<in C, S, E>(
override val decide: (C, S) -> Flow<E>,
override val evolve: (S, E) -> S,
override val initialState: S
) : IDecider<C, S, E>
Additionally, initialState
of the Decider is introduced to gain more control over the initial state of the Decider.
Notice that Decider
implements an interface IDecider
to communicate the contract.
Example
fun restaurantOrderDecider() = Decider<RestaurantOrderCommand?, RestaurantOrder?, RestaurantOrderEvent?>(
initialState = null,
decide = { c, s ->
when (c) {
is CreateRestaurantOrderCommand ->
if (s == null) flowOf(RestaurantOrderCreatedEvent(c.identifier, c.lineItems, c.restaurantIdentifier))
else flowOf(RestaurantOrderRejectedEvent(c.identifier, "Restaurant order already exists"))
is MarkRestaurantOrderAsPreparedCommand ->
if ((s != null && CREATED == s.status)) flowOf(RestaurantOrderPreparedEvent(c.identifier))
else flowOf(
RestaurantOrderNotPreparedEvent(
c.identifier,
"Restaurant order does not exist or not in CREATED state"
)
)
null -> emptyFlow()
}
},
evolve = { s, e ->
when (e) {
is RestaurantOrderCreatedEvent -> RestaurantOrder(e.identifier, e.restaurantId, CREATED, e.lineItems)
is RestaurantOrderPreparedEvent -> s?.copy(status = PREPARED)
is RestaurantOrderErrorEvent -> s
null -> s
}
}
)
Decider extensions and functions
Contravariant
Decider<C, S, E>.mapLeftOnCommand(f: (Cn) -> C): Decider<Cn, S, E>
Profunctor (Contravariant and Covariant)
Decider<C, S, E>.dimapOnEvent(fl: (En) -> E, fr: (E) -> En): Decider<C, S, En>
Decider<C, S, E>.dimapOnState(fl: (Sn) -> S, fr: (S) -> Sn): Decider<C, Sn, E>
Commutative Monoid
-
<reified Cx : C_SUPER, Sx, reified Ex : E_SUPER, reified Cy : C_SUPER, Sy, reified Ey : E_SUPER, C_SUPER> Decider<Cx?, Sx, Ex?>.combine( y: Decider<Cy?, Sy, Ey?> ): Decider<C_SUPER, Pair<Sx, Sy>, E_SUPER>
-
with identity element Decider<Nothing?, Unit, Nothing?>
A monoid is a type together with a binary operation (combine
) over that type, satisfying associativity and having an
identity/empty element.
Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed
in parallel.
combine
operation is also commutative. This means that the order in which deciders are combined does not affect the
result.
We can now construct event-sourcing or/and state-storing aggregate by using the same decider
.
Event-sourcing aggregate
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
extends IDecider
and EventRepository
interfaces, clearly communicating that it is composed
out of these two behaviours.
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
typealias RestaurantOrderAggregate = EventSourcingAggregate<RestaurantOrderCommand?, RestaurantOrder?, RestaurantOrderEvent?>
fun restaurantOrderAggregate(
restaurantOrderDecider: RestaurantOrderDecider,
eventRepository: EventRepository<RestaurantOrderCommand?, RestaurantOrderEvent?>
): RestaurantOrderAggregate = eventSourcingAggregate(
decider = restaurantOrderDecider,
eventRepository = eventRepository,
)
State-stored aggregate
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
extends IDecider
and StateRepository
interfaces, clearly communicating that it is composed
out of these two behaviours.
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 {}
Example
typealias RestaurantOrderAggregate = StateStoredAggregate<RestaurantOrderCommand?, RestaurantOrder?, RestaurantOrderEvent?>
fun restaurantOrderAggregate(
restaurantOrderDecider: RestaurantOrderDecider,
aggregateRepository: StateRepository<RestaurantOrderCommand?, RestaurantOrder?>
): RestaurantOrderAggregate = stateStoredAggregate(
decider = restaurantOrderDecider,
stateRepository = aggregateRepository
)
The logic is orchestrated on the application layer. The components/functions are composed in different ways to support
variety of requirements.
Check, application-vanilla and application-arrow modules/libraries for
scenarios that are offered out of the box.
View
View
is a datatype that represents the event handling algorithm, responsible for translating the events into
denormalized state, which is more adequate for querying. It belongs to the Domain layer. It is usually used to create
the view/query side of the CQRS pattern. Obviously, the command side of the CQRS is usually event-sourced aggregate.
It has two generic parameters S
, E
, representing the type of the values that View
may contain or use.
View
can be specialized for any type of S
, E
because these types do not affect its behavior.
View
behaves the same for E
=Int
or E
=YourCustomType
, for example.
View
is a pure domain component.
data class View<S, in E>(
override val evolve: (S, E) -> S,
override val initialState: S
) : IView<S, E>
Notice that View
implements an interface IView
to communicate the contract.
Example
fun restaurantOrderView() = View<RestaurantOrderViewState?, RestaurantOrderEvent?>(
initialState = null,
evolve = { s, e ->
when (e) {
is RestaurantOrderCreatedEvent -> RestaurantOrderViewState(
e.identifier,
e.restaurantId,
CREATED,
e.lineItems
)
is RestaurantOrderPreparedEvent -> s?.copy(status = PREPARED)
is RestaurantOrderErrorEvent -> s
null -> s
}
}
)
View extensions and functions
Contravariant
View<S, E>.mapLeftOnEvent(f: (En) -> E): View<S, En>
Profunctor (Contravariant and Covariant)
View<S, E>.dimapOnState(fl: (Sn) -> S, fr: (S) -> Sn): View<Sn, E>
Commutative Monoid
<Sx, reified Ex : E_SUPER, Sy, reified Ey : E_SUPER, E_SUPER> View<Sx, Ex?>.combine(y: View<Sy, Ey?>): View<Pair<Sx, Sy>, E_SUPER>
- with identity element
View<Unit, Nothing?>
A monoid is a type together with a binary operation (combine) over that type, satisfying associativity and having an
identity/empty element.
Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed
in parallel.
combine
operation is also commutative. This means that the order in which views are combined does not affect the
result.
We can now construct materialized
view by using this view
.
Materialized View
A Materialized view is
using/delegating a View
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 of the CQRS pattern. It belongs to the Application layer.
In order to handle the event, materialized view needs to fetch the current state via ViewStateRepository.fetchState
suspending function first, and then delegate the event to the view, which can produce new state as a result. New state
is then stored via ViewStateRepository.save
suspending function.
MaterializedView
extends IView
and ViewStateRepository
interfaces, clearly communicating that it is composed out
of these two behaviours.
The Delegation pattern has proven to be a good alternative to implementation inheritance
, and Kotlin supports it
natively requiring zero boilerplate code.
materializedView
function is a good example:
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 {}
Example
typealias RestaurantOrderMaterializedView = MaterializedView<RestaurantOrderViewState?, RestaurantOrderEvent?>
fun restaurantOrderMaterializedView(
restaurantOrderView: RestaurantOrderView,
viewStateRepository: ViewStateRepository<RestaurantOrderEvent?, RestaurantOrderViewState?>
): RestaurantOrderMaterializedView = materializedView(
view = restaurantOrderView,
viewStateRepository = viewStateRepository
)
The logic is orchestrated on the application layer. The components/functions are composed in different ways to support
variety of requirements.
Check, application-vanilla and application-arrow modules/libraries for
scenarios that are offered out of the box.
Saga
Saga
is a datatype that represents the central point of control, deciding what to execute next (A
). It is
responsible for mapping different events from many aggregates into action results AR
that the Saga
then can use to
calculate the next actions A
to be mapped to commands of other aggregates.
Saga
is stateless, it does not maintain the state.
It has two generic parameters AR
, A
, representing the type of the values that Saga
may contain or use.
Saga
can be specialized for any type of AR
, A
because these types do not affect its behavior.
Saga
behaves the same for AR
=Int
or AR
=YourCustomType
, for example.
Saga
is a pure domain component.
AR
- Action ResultA
- Action
data class Saga<AR, A>(
val react: (AR) -> Flow<A>
) : I_Saga<AR, A>
Notice that Saga
implements an interface ISaga
to communicate the contract.
Example
fun restaurantOrderSaga() = Saga<RestaurantEvent?, RestaurantOrderCommand>(
react = { e ->
when (e) {
is RestaurantOrderPlacedAtRestaurantEvent -> flowOf(
CreateRestaurantOrderCommand(
e.restaurantOrderId,
e.identifier,
e.lineItems
)
)
is RestaurantCreatedEvent -> emptyFlow()
is RestaurantMenuActivatedEvent -> emptyFlow()
is RestaurantMenuChangedEvent -> emptyFlow()
is RestaurantMenuPassivatedEvent -> emptyFlow()
is RestaurantErrorEvent -> emptyFlow()
null -> emptyFlow()
}
}
)
fun restaurantSaga() = Saga<RestaurantOrderEvent?, RestaurantCommand>(
react = { e ->
when (e) {
is RestaurantOrderCreatedEvent -> emptyFlow()
is RestaurantOrderPreparedEvent -> emptyFlow()
is RestaurantOrderErrorEvent -> emptyFlow()
null -> emptyFlow()
}
}
)
Saga extensions and functions
Contravariant
Saga<AR, A>.mapLeftOnActionResult(f: (ARn) -> AR): Saga<ARn, A>
Covariant
Saga<AR, A>.mapOnAction(f: (A) -> An): Saga<AR, An>
Monoid
<reified ARx : AR_SUPER, Ax : A_SUPER, reified ARy : AR_SUPER, Ay : A_SUPER, AR_SUPER, A_SUPER> Saga<in ARx?, out Ax>.combine(y: Saga<in ARy?, out Ay>): Saga<AR_SUPER, A_SUPER>
- with identity element
Saga<Nothing?, Nothing?>
A monoid is a type together with a binary operation (combine) over that type, satisfying associativity and having an
identity/empty element.
Associativity facilitates parallelization by giving us the freedom to break problems into chunks that can be computed
in parallel.
combine
operation is also commutative. This means that the order in which sagas are combined does not affect the
result.
We can now construct Saga Manager
by using this saga
.
Saga Manager
Saga manager is a stateless process
orchestrator. It is reacting on Action Results of type AR
and produces new actions A
based on them.
Saga manager is using/delegating a Saga
to react on Action Results of type AR
and produce new actions A
which are
going to be published via ActionPublisher.publish
suspending function.
It belongs to the Application layer.
SagaManager
extends ISaga
and ActionPublisher
interfaces, clearly communicating that it is composed out of these
two behaviours.
The Delegation pattern has proven to be a good alternative to implementation inheritance
, and Kotlin supports it
natively requiring zero boilerplate code.
sagaManager
function is a good example:
fun <AR, A> sagaManager(
saga: ISaga<AR, A>,
actionPublisher: ActionPublisher<A>
): SagaManager<AR, A> =
object : SagaManager<AR, A>, ActionPublisher<A> by actionPublisher, ISaga<AR, A> by saga {}
Example
typealias OrderRestaurantSagaManager = SagaManager<Event?, Command>
fun sagaManager(
restaurantOrderSaga: RestaurantOrderSaga,
restaurantSaga: RestaurantSaga,
actionPublisher: ActionPublisher<Command>
): OrderRestaurantSagaManager = sagaManager(
saga = restaurantOrderSaga.combine(restaurantSaga),
actionPublisher = actionPublisher
)
Experimental features
Actors (only on JVM)
Coroutines can be executed parallelly. It presents all the usual parallelism problems. The main problem being
synchronization of access to shared mutable
state. Actors to the rescue!
Dive into the implementation ...
private fun <C, E> CoroutineScope.commandActor(
fanInChannel: SendChannel<E>,
capacity: Int = Channel.RENDEZVOUS,
start: CoroutineStart = CoroutineStart.DEFAULT,
context: CoroutineContext = EmptyCoroutineContext,
handle: (C) -> Flow<E>
) = actor<C>(context, capacity, start) {
for (msg in channel) {
handle(msg).collect { fanInChannel.send(it) }
}
}
Actors
are marked as @ObsoleteCoroutinesApi by Kotlin at the moment.
Kotlin
"Kotlin has both object-oriented and functional constructs. You can use it in both OO and FP styles, or mix elements of
the two. With first-class support for features such as higher-order functions, function types and lambdas, Kotlin is a
great choice if you’re doing or exploring functional programming."
Start using the libraries
All fmodel
components/libraries are released to Maven Central
Maven coordinates
<dependency>
<groupId>com.fraktalio.fmodel</groupId>
<artifactId>domain</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.fraktalio.fmodel</groupId>
<artifactId>application-vanilla</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.fraktalio.fmodel</groupId>
<artifactId>application-arrow</artifactId>
<version>3.5.1</version>
</dependency>
Examples
- Browse the tests
- Learn by example on the playground
- Read the blog
- Check the demos
- Spring, R2DBC, Event Sourcing, CQRS, Postgres
- Spring, R2DBC, State-Stored, Postgres
- Ktor, R2DBC, Event Sourcing, CQRS, Postgres
FModel in other languages
References and further reading
Credits
Special credits to Jérémie Chassaing
for sharing his research
and Adam Dymitruk
for hosting the meetup.
Created with :heart: by Fraktalio