Occurrent 0.17.0 is released.
Table of Contents
Decider Support
Occurrent now has basic, somewhat experimental, support for Deciders (expect more functionality in the future). A decider is a model that can be implemented to get a structured way to implement decision logic for a business entity (typically aggregate) or use case.
To use a decider, you need to model your commands as explicit data structures (i.e. don’t use higher-order function).
To create a decider, first depend on org.occurrent:decider:0.17.0
, then you can either implement the org.occurrent.dsl.decider.Decider
interface or use the default implementation. The interface is defined like this:
public interface Decider<C, S, E> {
S initialState();
@NotNull
List<E> decide(@NotNull C command, S state);
S evolve(S state, @NotNull E event);
default boolean isTerminal(S state) {
return false;
}
}
where:
Parameter Type | Description |
---|---|
C | The type of the commands that the decider can handle |
S | The state that the decider works with |
E | The type of events that the decider returns |
The interface contains four methods:
Method name | Description |
---|---|
initialState | Returns the initial state of the decider, for example null or something like “NotStarted ” (a domain specific state implemented by you), depending on your domain |
decide | A function that takes a command and the current state and returns a list of new events that represents the changes the occurred after the commands was handled |
evolve | A method that takes the current state and an event, and return an update state after applying this event |
isTerminal | An optional method that can be implemented/overridden to tell the Decider to stop evolving the state if the Decider has reached a specific state |
It’s highly recommended to read this blog post to get a better understanding of the rationale behind Deciders.
But you don’t actually need to implement this interface yourself, instead you can create a default implementation by passing in functions to Decider.create(..)
.
Java
Imagine that you have commands, events and state defined like this:
sealed interface Command {
record Command1(String something, String message) implements Command {
}
record Command2(String message) implements Command {
}
}
sealed interface Event {
record Event1(String something) implements Command {
}
record Event2(String somethingElse) implements Command {
}
record Event3(String message) implements Command {
}
}
record State(String something, String somethingElse, String message) {
// Other constructors excluded for brevity
}
Then you can create a decider like this in Java (21+):
var decider = Decider.<Command, State, Event>create(
null,
(command, state) -> switch (command) {
case Command1 c1 -> {
if (s == null) {
yield List.of(new Event1(c1.something()));
} else {
yield List.of(new Event3(c1.message()));
}
}
case Command2 c2 -> List.of(new MyEvent2(c2.somethingElse()));
},
(state, event) -> switch (event) {
case Event1 e1 -> new State(e1.something());
case Event2 e2 -> new State(s.something(), e2.message());
case Event3 e3 -> new State(s.something(), e3.somethingElse(), s.message());
}
);
Now that you have an instance of Decider
, you can then call any of the many default methods to return either the name state, the new events, or both. For example:
List<Event> currentEvents = ...
Command command = ..
List<Event> newEvents = decider.decideOnEventsAndReturnEvents(currentEvents, command);
State newState = decider.decideOnEventsAndReturnState(currentEvents, command);
// Return both the state and the new events
Decision<State, List<Event>> decision = decider.decideOnEvents(currentEvents, command);
Or if you store state instead of events:
State currentState = ...
Command command = ..
List<Event> newEvents = decider.decideOnStateAndReturnEvents(currentState, command);
State newState = decider.decideOnStateAndReturnState(currentState, command);
// Return both the state and the new events
Decision<State, List<Event>> decision = decider.decideOnState(currentState, command);
You can even apply multiple commands at the same time:
List<Event> currentEvents = ...
Command command1 = ..
Command command2 = ..
List<Event> newEvents = decider.decideOnEventsAndReturnEvents(currentEvents, command1, command2);
Then both commands will be applied atomically.
Application Service from Java
To use the existing ApplicationService infrastructure with Deciders, you can do like this:
ApplicationService<Event> applicationService = ...
Command command = ...
// Because the decider expects a List<Event>, and not Stream<Event> as expected by the ApplicationService,
// we first convert the Stream to a List using the "toStreamCommand" function provided by Occurrent.
var writeResult = applicationService.execute("streamId", toStreamCommand(events -> decider.decideOnEventsAndReturnEvents(events, defineName)));
toStreamCommand
can be statically imported from org.occurrent.application.composition.command.toStreamCommand
.
Kotlin
The org.occurrent:decider:0.17.0
module contains Kotlin extension functions, located in the org.occurrent.dsl.decider.DeciderExtensions.kt
file, that makes deciders more idiomatic to work with from Kotlin.
Imagine that you have commands, events and state defined like this:
sealed interface Command {
data class Command1(val something : String, val message : String) : Command
data class Command2(val message : String) : Command
}
sealed interface Event {
data class Event1(val something : String)
data class Event2(val somethingElse : String)
data class Event3(val message : String)
}
data class State(val something : String, val somethingElse : String, val message : String) {
// Other constructors excluded for brevity
}
Then you can create a decider like this in Kotlin:
import org.occurrent.dsl.decider.decider
val decider = decider<Command, State?, Event>(
initialState = null,
decide = { cmd, state ->
when (cmd) {
is Command1 -> listOf(if (cmd == null) Event1(c1.something()) else Event3(c1.message()))
is Command2 -> listOf(MyEvent2(c2.somethingElse()))
}
},
evolve = { _, e ->
when (e) {
is Event1 -> State(e1.something())
is Event2 -> State(s.something(), e2.message())
is Event3 -> State(s.something(), e3.somethingElse(), s.message())
}
}
)
isTerminal
predicate as a fourth argument to the decider(..)
function if you need to specify this condition, otherwise it always returns false
by default.
Now that you have an instance of Decider
, you can then call any of the many default methods to return either the name state, the new events, or both. For example:
import org.occurrent.dsl.decider.decide
import org.occurrent.dsl.decider.component1
import org.occurrent.dsl.decider.component2
val currentEvents : List<Event> = ...
val currentState : State? = ...
val command : Command = ...
// We use destructuring here to get the "newState" and "newEvents" from the Decision instance returned by decide
// This is the reason for importing the "component1" and "component2" extension functions above
val (newState, newEvents) = decider.decide(events = currentEvents, command = command)
// You can also start the computation based on the current state
val (newState, newEvents) = decider.decide(state = currentState, command = command)
// And you could of course also just use the actual "Decision" if you like
val decision : Decision<State, List<Event>> = decider.decide(events = currentEvents, command = command)
// You can also supply multiple commands at the same time, then all of them will succeed or fail atomically
val (newState, newEvents) = decider.decide(currentState, command1, command2)
decide
methods, such as decideOnStateAndReturnEvent
, from Kotlin, but usually it's enough to just use the org.occurrent.dsl.decider.decide
extension function.
Application Service from Kotlin
The org.occurrent:decider:0.17.0
module contains Kotlin extension functions, located in the org.occurrent.dsl.decider.ApplicationServiceDeciderExtensions.kt
file, that allows you to easily integrate deciders
with existing ApplicationService infrastructure. Here’s an example:
import org.occurrent.dsl.decider.execute
// Create the decider
val applicationService = ...
val decider = ...
// Then you can pass the decider and command to the application service instance
val writeResult = applicationService.execute(streamId, command, decider)
It’s also possible to return the decision, state or new events when calling execute:
import org.occurrent.dsl.decider.executeAndReturnDecision
import org.occurrent.dsl.decider.executeAndReturnState
import org.occurrent.dsl.decider.executeAndReturnEvents
// Invoke the decider with the command and return both state and new events (decision)
val decision = applicationService.executeAndReturnDecision(streamId, command, decider)
// Invoke the decider with the command and return the new state
val state = applicationService.executeAndReturnState(streamId, command, decider)
// Invoke the decider with the command and return the new events
val newEvents = applicationService.executeAndReturnEvents(streamId, command, decider)
Changes
spring-boot-starter-mongodb
no longer autoconfigures itself by just importing the library in the classpath, instead you need to bootstrap by annotating your Spring Boot class with@EnableOccurrent
. Warning This is a non-backward compatible change!-
Subscriptions DSL now accepts metadata as the first parameter when calling
subscribe
to create a new subscription, besides just the event. The metadata currently contains the stream version and stream id, which can be useful when building projections. For example:subscriptions(subscriptionModel, cloudEventConverter) { subscribe<GameStarted>("id1") { metadata, gameStarted -> log.info("Game was started $gameStarted, event version was ${metadata.streamVersion}") } }
metadata
is an instance oforg.occurrent.dsl.subscription.blocking.EventMetadata
, and besidesstreamVersion
, it also includesstreamId
, and all other cloud event extensions properties you’ve added when serializing theCloudEvent
. All these properties are available in thedata
property.
Bug Fixes
- Fixed bug in spring-boot-starter-mongodb module in which it didn’t automatically configure MongoDB.
- Fixed a bug in SpringMongoSubscriptionModel in which it didn’t restart correctly on non DataAccessException’s
- Fixed a rare ConcurrentModificationException issue in SpringMongoSubscriptionModel if the subscription model is shutdown while it’s restarting
Upgrades
- Upgraded from Kotlin 1.9.20 to 1.9.22
- Upgraded amqp-client from 5.16.0 to 5.20.0
- Upgraded Spring Boot from 3.1.4 to 3.2.1
- Upgraded reactor from 3.5.10 to 3.6.0
- Upgraded Spring data MongoDB from 4.1.4 to 4.2.0
- Upgraded jobrunr from 6.3.2 to 6.3.3
- Upgraded mongodb drivers from 4.10.2 to 4.11.1
- Upgraded lettuce core from 6.2.6.RELEASE to 6.3.1.RELEASE
- Upgraded spring-aspects from 6.0.10 to 6.1.1
- Upgraded jackson from 2.15.2 to 2.15.3
Predicate
as a fourth argument toDecider.create(..)
if you like to specify theisTerminal
condition, otherwise it always returnsfalse
by default.