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());    
        }
);
You can pass an optional Predicate as a fourth argument to Decider.create(..) if you like to specify the isTerminal 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:

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())
            }
        }
)
You can also, optionally, define a 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)
You can also use the Java 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 of org.occurrent.dsl.subscription.blocking.EventMetadata, and besides streamVersion, it also includes streamId, and all other cloud event extensions properties you’ve added when serializing the CloudEvent. All these properties are available in the data 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