Skip to content

Repository files navigation

f(model) - Functional and Reactive Domain Modeling with Java

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 library, or as an inspiration, or both. It provides just enough tactical Domain-Driven Design patterns, optimised for Event Sourcing and CQRS.

  • The domain module/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 application module/package orchestrates the execution of the logic by loading state, executing domain components and storing new state. It is written in Java programming language.

onion architecture image

Experimental

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

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 - Command
  • S - State
  • E - Event
publicrecordDecider<C, S, E>(BiFunction<C, S, List<E>> decide,
BiFunction<S, E, S> evolve,
Supplier<S> initialState
) implementsIDecider<C, S, E> {
}

decider image

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

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 - State
  • E - Event
publicrecordView<S, E>(BiFunction<S, E, S> evolveView,
Supplier<S> initialViewState
) implementsIView<S, E> {
}

view image

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

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 Result
  • A - Action
publicrecordSaga<AR, A>(Function<AR, Stream<A>> react) implementsISaga<AR, A> {
}

saga image

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());
}
}

Requirements

  • Java 21

Driven by Maven

./mvnw clean verify

Examples

Check tests

FModel in other languages

FModel is ported to other programming languages. This section contains links to the documentation.

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 ❤️ by Fraktalio

About

f(model) - Functional and Reactive domain modeling with Java

Topics

Resources

Stars

25 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages