Skip to main content
Version: Previous

State Machine - advanced

The State Machine can be injected to the Event Handler script or API.

StateMachineBuilder

You can build a State Machine using this interface by providing:

  • the entity of the State Machine. In the example below, it is called Trade.
  • the state of the entity field to manage; this field needs to be ENUM field. In the example below, it is called TradeStatus.
  • the side effect of a state change, which is something you can subscribe to when the state changes. In the example below, it is called TradeEffect.
sealed class TradeEffect {
object New : TradeEffect()
object Allocated : TradeEffect()
object Cancelled : TradeEffect()
}

The following functions are available:

readState

fun readState(init: T.() -> S) This specifies which state to read from table.

state

fun state(state: S, stateHandler: StateBuilder<T, S, E>.() -> Unit) This is the method where you define how to handle the state of the table. Handling of the state is managed by StateBuilder, which is explained in detail below.

StateBuilder

SignatureDescription
isMutablevar isMutable: BooleanWhen set to false, any modification to the entity while in this state will be rejected
initialStatefun initialState(sideEffect: E, transitionBuilder: TransitionBuilderInit<T>? = null)The State Machine will accept the specified state on creation
initialStatefun initialState(sideEffect: E,priority: Int,transitionBuilder: TransitionBuilderInit<T>? = null)The State Machine will accept the specified state on creation and you can specify priority for this transition
initialStateWithContextfun <C : Any> initialStateWithContext(sideEffect: E,transitionBuilder: ContextTransitionBuilder<T, C>.() -> Unit,)The State Machine will accept the current state on creation. The validation will share a context.
initialStateWithContextfun <C : Any> initialStateWithContext(sideEffect: E,priority: Int,transitionBuilder: ContextTransitionBuilder<T, C>.() -> Unit,)The State Machine will accept the current state on creation and you can specify priority for this transition. The validation will share a context.
transitionfun transition(newState: S,sideEffect: E,transitionBuilder: TransitionBuilderInit<T>? = null,)The State Machine will transition from the current state to [newState]
transitionfun transition(newState: S,sideEffect: E,priority: Int,transitionBuilder: TransitionBuilderInit<T>? = null,)The State Machine will transition from the current state to [newState], and you can specify priority for this transition
transitionWithContextfun <C : Any> transitionWithContext(newState: S,sideEffect: E,transitionBuilder: ContextTransitionBuilder<T, C>.() -> Unit,) The State Machine will transition from the current state to [newState]
transitionWithContextfun <C : Any> transitionWithContext(newState: S,sideEffect: E,priority: Int,transitionBuilder: ContextTransitionBuilder<T, C>.() -> Unit,)The State Machine will transition from the current state to [newState], and you can specify priority for this transition. The validation will share a context

Here is an example of creating a State Machine.

@Module
class TradeStateMachine @Inject constructor(
db: AsyncEntityDb
) {
private val internalState: StateMachine<Trade, TradeStatus, TradeEffect> = db.stateMachineBuilder {
// Read TRADE_STATUS from Trade table
readState { tradeStatus }

// Defines how the status NEW is handled
state(TradeStatus.NEW) {
// Allows the status to change
isMutable = true

// State machine will accept the specified state on creation of trade
initialState(TradeEffect.New) {
onValidate { trade ->
if (trade.isTradeIdInitialised && trade.tradeId.isNotEmpty()) {
verify {
db hasNoEntry Trade.ById(trade.tradeId)
}
}
}
}

onCommit { trade ->
if (trade.enteredBy == "TestUser") {
trade.price = 10.0
}
}

// Allowed transitions are from NEW to ALLOCATED
transition(TradeStatus.ALLOCATED, TradeEffect.Allocated)
// Allowed transitions are from NEW to CANCELLED
transition(TradeStatus.CANCELLED, TradeEffect.Cancelled)
}

// Defines how the status ALLOCATED is handled
state(TradeStatus.ALLOCATED) {
// Doesn't allow the status to change
isMutable = false

transition(TradeStatus.NEW, TradeEffect.New)
transition(TradeStatus.CANCELLED, TradeEffect.Cancelled)
}

// Defines how the status CANCELLED is handled
state(TradeStatus.CANCELLED) {
// Doesn't allow the status to change
isMutable = false
}
}
}

StateMachine

State Machine properties and functions:

SignatureDescription
sideEffectsval sideEffects: Set<E>Provides all possible side effects
subscribefun subscribe(): Flow<Transition<T, S, E>>Subscribers will be notified of state transitions
highestPriorityTransitionsuspend fun highestPriorityTransition(value: T,): ValidationResult<S>?Given the table name, this gives the highest priority transition of the table if present or null
currentStatesuspend fun currentState(value: T): S?Reads the current state [S] of [value] [T] from the database
createsuspend fun create(value: T): Transition<T, S, E>Creates a new entity in the database.
updatesuspend fun update(value: T): Transition<T, S, E>?Will validate and write [value] to the database. Returns a [Transition] if the update caused a transition, else null and throws IllegalArgumentException if the value is not valid
updatesuspend fun update(uniqueEntityIndex: UniqueEntityIndex<T, *>,updateEntity: suspend (entity: T, transaction: AsyncMultiEntityReadWriteGenericSupport) -> Unit,): Transition<T, S, E>?Accepts a [UniqueEntityIndex] and an update lambda, and validates and writes [value] to the database. Returns a [Transition] if the update caused a transition, else null and throws IllegalArgumentException if the value is not valid, or if the item doesn't exist in the database.
updateIfExistssuspend fun updateIfExists(uniqueEntityIndex: UniqueEntityIndex<T, *>,updateEntity: suspend (entity: T, transaction: AsyncMultiEntityReadWriteGenericSupport) -> Unit,): Transition<T, S, E>?Accepts a [UniqueEntityIndex] and an update lambda and validates and writes [value] to the database. Returns a [Transition] if the update caused a transition, else null and throws IllegalArgumentException if the value is not valid
validatesuspend fun validate(value: T,newState: S,): ValidationResult<S>Validates if [value] can successfully transition to [newState]
withTransactionsuspend fun <O : Any?> withTransaction(transaction: AsyncMultiEntityReadWriteGenericSupport,handling: suspend StateMachine<T, S, E>.() -> O,): OWill take a transaction and will handle all State Machine calls within [handling] within the provided transaction
withTransactionsuspend fun <O : Any?> withTransaction(handling: suspend StateMachine<T, S, E>.() -> O,): OWill create a transaction and will handle all State Machine calls within [handling] within the provided transaction

Here is an example of a State Machine, where you can see properties and functions in action:

suspend fun insert(trade: Trade): Transition<Trade, TradeStatus, TradeEffect> = internalState.create(trade)

suspend fun modify(tradeId: String, modify: suspend (Trade) -> Unit): Transition<Trade, TradeStatus, TradeEffect>? =
internalState.update(Trade.ById(tradeId)) { trade, _ -> modify(trade) }

suspend fun modify(trade: Trade): Transition<Trade, TradeStatus, TradeEffect>? = internalState.update(trade)

Inject State Machine

Inject State Machine in Event Handler file

eventHandler {
val stateMachine = inject<TradeStateMachine>()

eventHandler<Trade>(name = "TRADE_INSERT", transactional = true) {
permissioning {
permissionCodes = listOf("TRADER")
auth(mapName = "ENTITY_VISIBILITY") {
field { counterpartyId }
}
}
onValidate { event ->
val message = event.details
verify {
entityDb hasEntry Counterparty.ById(message.counterpartyId)
entityDb hasEntry Instrument.ById(message.instrumentId)
}
ack()
}
onCommit { event ->
val trade = event.details
trade.enteredBy = event.userName
stateMachine.insert(entityDb, trade)
ack()
}
}
}

Inject State Machine in Event Handler API

@Module
class EventCompanyHandlerAsync @Inject constructor(
private val entityDb: AsyncEntityDb,
private val tradeMachine: TradeStateMachine
) : AsyncEventHandler<Company, EventReply> {
override suspend fun process(message: Event<Company>): EventReply {
val company = message.details
// custom code block..
return EventReply.EventAck()
}
}