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.
This is
main
branch, a home of version 2. It is areactive
version of libraries, optimized for streaming.
The v1 branch does not depend on Kotlin coroutines or Flow within the domain model.
This project 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.
- 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
library orchestrates the execution of the logic by loading state, executingdomain
components and storing new state. It is written in Kotlin programming language, with Arrow as additional dependency.
- f(model) - Functional domain modeling
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.
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 newEvents/E
to be published/emitted on the output
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
- 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.
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.
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:
(Flow<E>) -> S
We can now use this function (Flow<E>) -> S
to:
- contra-map our
decide
function ((C, S) -> Flow<E>
) overS
type to:(C, Flow<E>) -> Flow<E>
- ** this is an event-sourced system** - or to map our
decide
function ((C, S) -> Flow<E>
) overE
type to:(C, S) -> S
- this is a state-stored system
We can verify that we can design any information system (event-sourced or/and state-stored) in this way by using these two functions 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: suspend (S, E) -> S,
)
Decider
is the most important datatype, but it is not the only one. There are others:
_Decider
is a datatype that represents the main decision-making algorithm. It belongs to the Domain layer. It has five
generic parameters C
, Si
, So
, Ei
, Eo
, representing the type of the values that _Decider
may contain or use.
_Decider
can be specialized for any type C
or Si
or So
or Ei
or Eo
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
- CommandSi
- input StateSo
- output StateEi
- input EventEo
- output Event
We make a difference between input and output types, and we are more general in this case. We can always specialize down
to the 3 generic parameters: typealias Decider<C, S, E> = _Decider<C, S, S, E, E>
data class _Decider<C, Si, So, Ei, Eo>(
val decide: (C, Si) -> Flow<Eo>,
val evolve: suspend (Si, Ei) -> So,
val initialState: So
)
typealias Decider<C, S, E> = _Decider<C, S, S, E, E>
Additionally, initialState
of the Decider is introduced to gain more control over the initial state of the Decider.
Decider<C, Si, So, Ei, Eo>.mapLeftOnCommand(f: (Cn) -> C): Decider<Cn, Si, So, Ei, Eo>
Decider<C, Si, So, Ei, Eo>.dimapOnEvent( fl: (Ein) -> Ei, fr: (Eo) -> Eon ): Decider<C, Si, So, Ein, Eon>
Decider<C, Si, So, Ei, Eo>.mapLeftOnEvent(f: (Ein) -> Ei): Decider<C, Si, So, Ein, Eo>
Decider<C, Si, So, Ei, Eo>.mapOnEvent(f: (Eo) -> Eon): Decider<C, Si, So, Ei, Eon>
Decider<C, Si, So, Ei, Eo>.dimapOnState( fl: (Sin) -> Si, fr: (So) -> Son ): Decider<C, Sin, Son, Ei, Eo>
Decider<C, Si, So, Ei, Eo>.mapLeftOnState(f: (Sin) -> Si): Decider<C, Sin, So, Ei, Eo>
Decider<C, Si, So, Ei, Eo>.mapOnState(f: (So) -> Son): Decider<C, Si, Son, Ei, Eo>
rjustOnS(so: So): Decider<C, Si, So, Ei, Eo>
Decider<C, Si, So, Ei, Eo>.applyOnState(ff: Decider<C, Si, (So) -> Son, Ei, Eo>): Decider<C, Si, Son, Ei, Eo>
Decider<C, Si, So, Ei, Eo>.productOnState(fb: Decider<C, Si, Son, Ei, Eo>): Decider<C, Si, Pair<So, Son>, Ei, Eo>
-
Decider<in C?, in Si, out So, in Ei?, out Eo>.combine( y: Decider<in Cn?, in Sin, out Son, in Ein?, out Eon> ): Decider<C_SUPER, Pair<Si, Sin>, Pair<So, Son>, Ei_SUPER, Eo_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.
We can now construct event-sourcing or/and state-storing aggregate by using the same decider
.
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
implements an interface EventRepository
by delegating all of its public members to a
specified object. The Delegation pattern has proven to be a good alternative to implementation inheritance, and Kotlin
supports it natively requiring zero boilerplate code.
The by
-clause in the supertype list for EventSourcingAggregate
indicates that eventRepository
will be stored
internally in objects of EventSourcingAggregate
and the compiler will generate all the methods of EventRepository
that forward to eventRepository
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
implements an interface StateRepository
by delegating all of its public members to a specified
object. The Delegation pattern has proven to be a good alternative to implementation inheritance, and Kotlin supports it
natively requiring zero boilerplate code.
The by
-clause in the supertype list for StateStoredAggregate
indicates that aggregateStateRepository
will be
stored internally in objects of StateStoredAggregate
and the compiler will generate all the methods
of StateRepository
that forward to stateRepository
_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 three generic parameters Si
, So
, E
, representing the type of the values that _View
may contain or use.
_View
can be specialized for any type of Si
, So
, 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.
Si
- input StateSo
- output StateE
- Event
We make a difference between input and output types, and we are more general in this case. We can always specialize down
to the 2 generic parameters: typealias View<S, E> = _View<S, S, E>
data class _View<Si, So, E>(
val evolve: suspend (Si, E) -> So,
val initialState: So,
)
typealias View<S, E> = _View<S, S, E>
View<Si, So, E>.mapLeftOnEvent(f: (En) -> E): View<Si, So, En>
View<Si, So, E>.dimapOnState( fl: (Sin) -> Si, fr: (So) -> Son ): View<Sin, Son, E>
View<Si, So, E>.mapLeftOnState(f: (Sin) -> Si): View<Sin, So, E>
View<Si, So, E>.mapOnState(f: (So) -> Son): View<Si, Son, E>
View<Si, So, E>.applyOnState(ff: View<Si, (So) -> Son, E>): View<Si, Son, E>
justOnState(so: So): View<Si, So, E>
View<in Si, out So, in E?>.combine(y: View<in Si2, out So2, in E2?>): View<Pair<Si, Si2>, Pair<So, So2>, 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.
We can now construct materialized
view by using this 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.
_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>
)
typealias Saga<AR, A> = _Saga<AR, A>
Saga<AR, A>.mapLeftOnActionResult(f: (ARn) -> AR): Saga<ARn, A>
Saga<AR, A>.mapOnAction(f: (A) -> An): Saga<AR, An>
Saga<in AR?, out A>.combine(y: _Saga<in ARn?, out An>): Saga<AR_SUPER, A_SUPER>
- with identity element
Saga<Nothing?, Nothing?>
We can now construct Saga Manager
by using this saga
.
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.
"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."
All fmodel
components/libraries are released to Maven Central
<dependency>
<groupId>com.fraktalio.fmodel</groupId>
<artifactId>domain</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.fraktalio.fmodel</groupId>
<artifactId>application</artifactId>
<version>2.0.0</version>
</dependency>
mvn clean deploy -Dgpg.passphrase="YOUR_PASSPHRASE" -Pci-cd
- https://www.youtube.com/watch?v=kgYGMVDHQHs
- https://www.manning.com/books/functional-and-reactive-domain-modeling
- https://www.manning.com/books/functional-programming-in-kotlin
- https://www.47deg.com/blog/functional-domain-modeling/
- https://www.47deg.com/blog/functional-domain-modeling-part-2/
- https://www.youtube.com/watch?v=I8LbkfSSR58&list=PLbgaMIhjbmEnaH_LTkxLI7FMa2HsnawM_
Special credits to Jérémie Chassaing
for sharing his research
and Adam Dymitruk
for hosting the meetup.
Created with ❤️ by Fraktalio