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 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
domainmodule/package is fully isolated from the application layer and API-related concerns. It represents a pure declaration of the program logic. It is written in Java programming language, without additional dependencies. - The
applicationmodule/package orchestrates the execution of the logic by loading state, executingdomaincomponents and storing new state. It is written in Java programming language.
This project is in experimental phase, and it is not published to Maven Central.
It is using Amber productivity-oriented features to accelerate development of applications:
- records
- sealed hierarchy
- exhaustive pattern matching for Switch expressions
We plan to use Project Loom within the Application module/package to enable high-throughput lightweight concurrency and new programming models on the Java platform.
Please refer to kotlin or typescript or rust production ready versions of the libraries.
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
publicrecordDecider<C, S, E>(BiFunction<C, S, List<E>> decide,
BiFunction<S, E, S> evolve,
Supplier<S> initialState
) implementsIDecider<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 / Given-When-Then Test
A fluent test DSL/builder to support Given-When-Then format
classDeciderTest {
@TestvoiddeciderTest() {
varaddOddNumberCommand = newAddOddNumberCommand(1);
varoddNumberAddedEvent = newOddNumberAddedEvent(1);
varaddEvenNumberCommand = newAddEvenNumberCommand(2);
varevenNumberAddedEvent = newEvenNumberAddedEvent(2);
varoddState = newOddNumberState(0);
varevenState = newEvenNumberState(0);
varstate = newNumberState(evenState, oddState);
Decider<? superOddCommand, OddNumberState, OddEvent> oddDecider = newDecider<>(
(c, s) -> switch (c) {
caseAddOddNumberCommandcmd -> List.of(newOddNumberAddedEvent(s.value() + cmd.value()));
caseMultiplyOddNumberCommandcmd -> List.of(newOddNumberMultipliedEvent(s.value() * cmd.value()));
casenull -> List.of();
},
(s, e) -> switch (e) {
caseOddNumberAddedEventevt -> newOddNumberState(evt.value());
caseOddNumberMultipliedEventevt -> newOddNumberState(evt.value());
casenull -> s;
},
() -> oddState
);
Decider<? superEvenCommand, EvenNumberState, EvenEvent> evenDecider = newDecider<>(
(c, s) -> switch (c) {
caseAddEvenNumberCommandcmd -> List.of(newEvenNumberAddedEvent(s.value() + cmd.value()));
caseMultiplyEvenNumberCommandcmd ->
List.of(newEvenNumberMultipliedEvent(s.value() * cmd.value()));
casenull -> List.of();
},
(s, e) -> switch (e) {
caseEvenNumberAddedEventevt -> newEvenNumberState(evt.value());
caseEvenNumberMultipliedEventevt -> newEvenNumberState(evt.value());
casenull -> s;
},
() -> evenState
);
// Combining two deciders into oneDecider<Command, Pair<EvenNumberState, OddNumberState>, Event> _decider = Decider.combine(
evenDecider, EvenCommand.class, EvenEvent.class,
oddDecider, OddCommand.class, OddEvent.class
);
// Combining two deciders into one, plus mapping inconvenient `Pair` into a domain specific `NumberState`Decider<Command, NumberState, Event> decider = Decider
.combine(
evenDecider, EvenCommand.class, EvenEvent.class,
oddDecider, OddCommand.class, OddEvent.class)
.dimapState(
(ns) -> newPair<>(ns.evenNumber(), ns.oddNumber()),
(p) -> newNumberState(p.first(), p.second())
);
givenState(oddDecider, oddState)
.whenCommand(addOddNumberCommand)
.thenState(newOddNumberState(1));
givenEvents(oddDecider, List.of())
.whenCommand(addOddNumberCommand)
.thenEvents(List.of(oddNumberAddedEvent));
// Even decider: given evenState + addEvenNumberCommand -> then evenNumberAddedEventgivenState(evenDecider, evenState)
.whenCommand(addEvenNumberCommand)
.thenState(newEvenNumberState(2));
givenEvents(evenDecider, List.of())
.whenCommand(addEvenNumberCommand)
.thenEvents(List.of(evenNumberAddedEvent));
// Combined decider: given state + odd command -> eventsgivenEvents(decider, List.of())
.whenCommand(addOddNumberCommand)
.thenEvents(List.of(oddNumberAddedEvent));
// Combined decider: given state + odd command -> new stategivenState(decider, state)
.whenCommand(addOddNumberCommand)
.thenState(newNumberState(newEvenNumberState(0), newOddNumberState(1)));
// Combined decider: given state + even command -> new stategivenState(decider, state)
.whenCommand(addEvenNumberCommand)
.thenState(newNumberState(newEvenNumberState(2), newOddNumberState(0)));
}
}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.
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.
S- StateE- Event
publicrecordView<S, E>(BiFunction<S, E, S> evolveView,
Supplier<S> initialViewState
) implementsIView<S, E> {
}Notice that View implements an interface IView to
communicate the contract.
Example / Given-When-Then Test
A fluent test DSL/builder to support Given-When-Then format
classViewTest {
@TestvoidviewTest() {
varoddNumberAddedEvent = newOddNumberAddedEvent(1);
varevenNumberAddedEvent = newEvenNumberAddedEvent(2);
varoddState = newOddNumberState(0);
varevenState = newEvenNumberState(0);
varstate = newNumberState(evenState, oddState);
View<OddNumberState, ? superOddEvent> oddView = newView<>(
(s, e) -> switch (e) {
caseOddNumberAddedEventevt -> newOddNumberState(evt.value());
caseOddNumberMultipliedEventevt -> newOddNumberState(evt.value());
casenull -> s;
},
() -> oddState
);
View<EvenNumberState, ? superEvenEvent> evenView = newView<>(
(s, e) -> switch (e) {
caseEvenNumberAddedEventevt -> newEvenNumberState(evt.value());
caseEvenNumberMultipliedEventevt -> newEvenNumberState(evt.value());
casenull -> s;
},
() -> evenState
);
// Combining two views into oneView<Pair<EvenNumberState, OddNumberState>, ? superEvent> _view = View.combine(
evenView, EvenEvent.class,
oddView, OddEvent.class
);
// Combining two views into one, plus mapping inconvenient `Pair` into more domain specific `NumberState`View<NumberState, ? superEvent> view = View
.combine(evenView, EvenEvent.class, oddView, OddEvent.class)
.dimapState(
(ns) -> newPair<>(ns.evenNumber(), ns.oddNumber()),
(p) -> newNumberState(p.first(), p.second())
);
// --- DSL usage ---givenEvents(oddView, List.of(oddNumberAddedEvent))
.thenState(newOddNumberState(1));
givenEvents(evenView, List.of(evenNumberAddedEvent))
.thenState(newEvenNumberState(2));
givenEvents(view, List.of(oddNumberAddedEvent))
.thenState(newNumberState(newEvenNumberState(0), newOddNumberState(1)));
givenEvents(view, List.of(evenNumberAddedEvent))
.thenState(newNumberState(newEvenNumberState(2), newOddNumberState(0)));
}
}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
publicrecordSaga<AR, A>(Function<AR, Stream<A>> react) implementsISaga<AR, A> {
}Notice that Saga implements an interface ISaga to
communicate the contract.
Example
classSagaTest {
@TestvoidsagaTest() {
varoddNumberAddedEvent = newOddNumberAddedEvent(1);
varevenNumberAddedEvent = newEvenNumberAddedEvent(2);
varaddOddNumberCommand = newAddOddNumberCommand(3);
varaddEvenNumberCommand = newAddEvenNumberCommand(2);
Saga<? superOddEvent, ? extendsEvenCommand> oddSaga = newSaga<>(
(ar) -> switch (ar) {
caseOddNumberAddedEventevt -> Stream.of(newAddEvenNumberCommand(evt.value() + 1));
caseOddNumberMultipliedEventevt -> Stream.of(newMultiplyEvenNumberCommand(evt.value() + 1));
casenull -> Stream.empty();
}
);
Saga<? superEvenEvent, ? extendsOddCommand> evenSaga = newSaga<>(
(ar) -> switch (ar) {
caseEvenNumberAddedEventevt -> Stream.of(newAddOddNumberCommand(evt.value() + 1));
caseEvenNumberMultipliedEventevt -> Stream.of(newMultiplyOddNumberCommand(evt.value() + 1));
casenull -> Stream.empty();
}
);
// Combining two sagas into one sagaSaga<? superEvent, ? extendsCommand> saga = Saga.combine(
oddSaga, OddEvent.class,
evenSaga, EvenEvent.class
);
assertIterableEquals(List.of(addEvenNumberCommand), oddSaga.react().apply(oddNumberAddedEvent).toList());
assertIterableEquals(List.of(addOddNumberCommand), evenSaga.react().apply(evenNumberAddedEvent).toList());
assertIterableEquals(List.of(addEvenNumberCommand), saga.react().apply(oddNumberAddedEvent).toList());
assertIterableEquals(List.of(addOddNumberCommand), saga.react().apply(evenNumberAddedEvent).toList());
}
}- Java 21
./mvnw clean verifyCheck tests
FModel is ported to other programming languages. This section contains links to the documentation.
Special credits to Jérémie Chassaing for sharing his research
and Adam Dymitruk for hosting the meetup.
Created with ❤️ by Fraktalio



