Skip to main content

Workflow (State Machine)

Overview

The Genesis server state machine is used to define workflow for application's table entities. State machines work in conjunction with Event Handlers to ensure records for the entity follow the correct workflow at all times.

To give an example, you may be building an Order Management System, and need to define the lifecycle of your Order's. You may have various states for your order defined in an ENUM:

ORDER_STATUS:

  • NEW
  • ASSIGNED
  • PARTIALLY_FILLED
  • FULLY_FILLED
  • COMPLETED
  • CANCELLED

There are typically two aspects of related system logic to define in your state machine:

  1. Transitions ; Define what the valid transitions between the states are. Building on our example, you may need to configure that order must be ASSIGNED before it is moved to PARTIALLY_FILLED. Also, CANCELLED is an end state and thus has no valid transitions to another state.
  2. State Validation ; Validate that all the required fields are set to transition to a given state. For example, an Order must have ASSIGNED_TO set to a valid user in the system to transition to ASSIGNED.

Example configuration

State Machines can be written in-line within a GPAL event handler, this is the recommended approach when using GPAL in your application.

info

A StateMachine API is also available, which can be used if you decide to write your event handlers using the event handler API. See further down for examples and docs.

The following example of a State Machine defines five events that control the transition of orders from one state to another.

eventHandler {
stateMachine(ORDER.ORDER_STATUS) {
insertEvent { //This generates an EVENT called `EVENT_ORDER_INSERT`
initialStates(ORDER_STATUS.NEW, ORDER_STATUS.ASSIGNED) //You can only set the ORDER_STATUS to these values on an insert.

excludedFields {
ENTERED_BY
ENTERED_TIME
MODIFIED_BY
MODIFIED_TIME
}

//onEvent can be used to update the event details prior to onValidate and onCommit being run
//event.withDetails is an optional helpful function to edit the event record without prefixing everything with event.details
onEvent { event ->
event.withDetails {
enteredBy = event.userName
enteredTime = now()
}
}

//This defines the resulting insert event's onValidate function.
onValidate { order ->
verifyOnly { order hasField ORDER.PRICE }
verifyOnly { order hasField ORDER.QUANTITY greaterThan 0 }
if(order.orderState == OrderStatus.ASSIGNED) {
if(order.assignedTo == null) {
return onValidate@nack("Order must be assigned to set status of ASSIGNED")
}
}
}

}

modifyEvent {
mutableStates(OrderStatus.NEW, OrderStatus.ASSIGNED) //You can only modify an order when it is in state NEW or ASSIGNED

excludedFields {
ENTERED_BY
ENTERED_TIME
MODIFIED_BY
MODIFIED_TIME
}

//As this is a modify event and we know the event uses the table object, both event and the retrieved record are available in onEvent on modifyEvent and transactionEvents
onEvent { event, order ->
order.modifiedBy = event.userName
order.modifiedTime = now()
}

onValidate { order ->
verifyOnly { order hasField ORDER.PRICE }
verifyOnly { order hasField ORDER.QUANTITY greaterThan 0 }
if(order.orderState == OrderStatus.ASSIGNED) {
if(order.assignedTo == null) {
return onValidate@nack("Order must be assigned to someone to set status of ASSIGNED")
}
}
}
}

//An order can only go to ASSIGNED from NEW
transitionEvent(OrderStatus.ASSIGNED) {
fromStates(OrderStatus.NEW)

onEvent { event, order ->
order.modifiedBy = event.userName
order.modifiedTime = now()
}

onValidate { order ->
if(order.orderState == OrderStatus.ASSIGNED) {
if(order.assignedTo == null) {
return onValidate@nack("Order must be assigned to someone to set status of ASSIGNED")
}
}
}
}

//An order can only go to PARTIALLY_FILLED from ASSIGNED
transitionEvent(OrderStatus.PARTIALLY_FILLED) {
fromStates(OrderStatus.ASSIGNED)

onEvent { event, order ->
order.modifiedBy = event.userName
order.modifiedTime = now()
}

onValidate { order ->
require(order.quantityFilled > 0) { "Order must have some quantity filled to be set as PARTIALLY_FILLED" }
}
}

//An order can only go to FULLY_FILLED from PARTIALLY_FILLED
transitionEvent(OrderStatus.FULLY_FILLED) {
fromStates(OrderStatus.PARTIALLY_FILLED)

onEvent { event, order ->
order.modifiedBy = event.userName
order.modifiedTime = now()
}

onValidate { order ->
require(order.quantityFilled == order.quantity) { "Order must have quantity filled to the full order quantity to be set as FULLY_FILLED" }
}
}

//An order can go to COMPLETED from ASSIGNED, PARTIALLY_FILLED, FULLY_FILLED
transitionEvent(OrderStatus.COMPLETED) {
fromStates(OrderStatus.ASSIGNED, OrderStatus.PARTIALLY_FILLED, OrderStatus.FULLY_FILLED )

onEvent { event, order ->
order.modifiedBy = event.userName
order.modifiedTime = now()
}
}

//An order can go to CANCELLED from ASSIGNED, PARTIALLY_FILLED, FULLY_FILLED
transitionEvent(OrderStatus.CANCELLED) {
fromStates(OrderStatus.ASSIGNED, OrderStatus.PARTIALLY_FILLED, OrderStatus.FULLY_FILLED)

onEvent { event, order ->
order.modifiedBy = event.userName
order.modifiedTime = now()
}
}
}
}

Configuration options

State Machines can be written in-line within a GPAL event handler, else written in Kotlin and injected for use. Configuration options of both are detailed here.

stateMachine

Takes two parameters:

  1. tableField ; which is the field of a table which holds the state.
  2. transactional ; optional boolean parameter (default is true) which functions the same as defining transactional on a standard eventHandler`
eventHandler {
stateMachine(tableField = ORDER.ORDER_STATUS, transactional = true) {
...
}
}

insertEvent

Define an event on this state machine for inserting the entity. The resulting eventHandler will be named EVENT_<table_entity_name>_INSERT. In the below example the event would be called EVENT_ORDER_INSERT.

eventHandler {
stateMachine(tableField = ORDER.ORDER_STATUS, transactional = true) {
insertEvent {
...
}
...
}
}
initialStates

The valid values that can be set on the tableField on an insertEvent

  ...
insertEvent {
initialStates(ORDER_STATUS.NEW, ORDER_STATUS.ASSIGNED)
...
}
}
}
excludedFields

Removes fields defined on the table from the event. These fields should only be set by the events and not the client.

    insertEvent {
...
excludedFields {
ENTERED_BY
ENTERED_TIME
MODIFIED_BY
MODIFIED_TIME
}
...
}
onEvent

onEvent can be used to update the event details prior to onValidate and onCommit being run. It provides event information, which can be manipulated before onValidate and onCommit blocks are run.

    insertEvent {
...
onEvent { event ->
event.withDetails {
enteredBy = event.userName
enteredTime = now()
}
}
...
}
onValidate

Validate the inbound event details here. See the event handler onValidate documentation for the full usage of onValidate.

One difference from regular eventHandler onValidate block is that we don't have to return an ack, it will be implicitly returned where no exception is hit, or nack returned.

    insertEvent {
...
onValidate { order ->
verifyOnly { order hasField ORDER.PRICE }
verifyOnly { order hasField ORDER.QUANTITY greaterThan 0 }
if(order.orderState == OrderStatus.ASSIGNED) {
if(order.assignedTo == null) {
return onValidate@nack("Order must be assigned to set status of ASSIGNED")
}
}
}
...
}
onCommit

Commit the inbound event details here. See the event handler onCommit documentation for the full usage of onCommit.

This block is optional.

There are a couple of differences from regular eventHandler onCommit block.

  1. We don't have to write the main object to the DB. This will also happen implicitly
  2. We don't have to return an ack, it will be implicitly returned where no exception is hit, or nack returned.
    insertEvent {
...
onCommit { order ->
//You can perform other functionality prior to committing the object here.
}
...
}

modifyEvent

Define an event on this state machine for modifying the entity. The resulting eventHandler will be named EVENT_<table_entity_name>__MODIFY. In the below example the event would be called EVENT_ORDER_MODIFY.

eventHandler {
stateMachine(tableField = ORDER.ORDER_STATUS, transactional = true) {
...
modifyEvent {
...
}
...
}
}
mutableStates

The valid values that the table record can have set on the tableField on an modifyEvent.

For the example below, you can only modify an order when it has a status of NEW or ASSIGNED

   ...
modifyEvent {
mutableStates(OrderStatus.NEW, OrderStatus.ASSIGNED)
...
}
}
}

modifyEvent can also define the following same blocks as insertEvent. Click to see their descriptions for usage details:

transitionEvent

Specifying a transitionEvent will generate an event specifically for transitioning the status. These events define the possible transitions of the ORDER_STATUS field. The resulting eventHandler will be named EVENT_<table_entity_name>__<status being transitioned to>.

In the below example the event would be called EVENT_ORDER_ASSIGNED.

    transitionEvent(OrderStatus.ASSIGNED) {
...
}
fromStates

Defines the states we can transition from, to the state defined in the transitionEvent parameter. Use comma separation to define multiple states.

In the example below the Order.OrderStatus must be NEW for this event to validate.

    transitionEvent(OrderStatus.ASSIGNED) {
fromStates(OrderStatus.NEW)
...
}

transitionEvent can also define the following same blocks as insertEvent. Click to see their descriptions for usage details:

Client API

The State Machine is used to configure workflow states and works hand in hand with event handlers. There is no Client API specifically for a State Machine. Use the event handler client API to interact with your events.

Runtime configuration

In-line state Machines are defined as Event Handlers in your application's *-eventhandler.kts file and you shouldn't need to do any more.

If you have defined your state-machine in kotlin, make sure the event handler definition in processes.xml includes the .jar file containing the state machine in <classpath>.

See runtime configuration - processes for further details.

Performance

If better database response performance is required, you may also consider added a cache to the process. See runtime configuration - process cache for more details.

Integration testing

A State Machine is just another layer of an Event Handler, the test configuration is same as for Event Handlers. You should add further tests to test your transition workflow logic works as anticipated.

See the event handlers testing section for more details.

StateMachine API

The Genesis Application Platform also offers an API for writing your state machine in kotlin. This approach is best used when you have written your event handlers using the event handler API

Example code

The following is an example State Machine written in kotlin. The functionality example mirrors the in-line GPAL example configuration described previously.

@Module
class OrderStateMachine @Inject constructor(
db: AsyncEntityDb
) {
private val internalState: StateMachine<Order, OrderStatus, OrderEffect> = db.stateMachineBuilder {
// Read ORDER_STATUS from Order table
readState { orderStatus }

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

// State machine will accept the specified state on creation of order
initialState(OrderEffect.New) {
onValidate { order ->
verifyOnly { order hasField ORDER.PRICE }
verifyOnly { order hasField ORDER.QUANTITY greaterThan 0 }
}
}

onCommit { order ->
event.withDetails {
enteredBy = event.userName
enteredTime = now()
}
}

// Allowed transitions are from NEW to ASSIGNED
transition(OrderStatus.ASSIGNED, OrderEffect.Assigned)
}

state(OrderStatus.ASSIGNED) {
// Allows the status to change
isMutable = true

//State machine will accept the specified state on creation of order
//Here we have our onValidate outside and will always run regardless of this being an insert or transition.
// initialState.onValidate on the NEW as it will only be on an insert. Assigned can be transitioned to.
initialState(OrderEffect.Assigned)

onValidate { order ->
verifyOnly { order hasField ORDER.PRICE }
verifyOnly { order hasField ORDER.QUANTITY greaterThan 0 }
if(order.orderState == OrderStatus.ASSIGNED) {
if(order.assignedTo == null) {
return onValidate@nack("Order must be assigned to set status of ASSIGNED")
}
}
}

onCommit { order ->
event.withDetails {
enteredBy = event.userName
enteredTime = now()
}
}

// Allowed transitions from ASSIGNED
transition(OrderStatus.PARTIALLY_FILLED, OrderEffect.PartiallyFilled)
transition(OrderStatus.COMPLETED, OrderEffect.Completed)
transition(OrderStatus.CANCELLED, OrderEffect.Cancelled)
}

state(OrderStatus.PARTIALLY_FILLED) {
isMutable = true

onValidate { order ->
require(order.quantityFilled > 0) { "Order must have some quantity filled to be set as PARTIALLY_FILLED" }
}

//Here we are using the onCommit, there is no onEvent when using the state machine API
onCommit { order ->
event.withDetails {
modifiedBy = event.userName
modifiedTime = now()
}
}

transition(OrderStatus.FULLY_FILLED, OrderEffect.Assigned)
transition(OrderStatus.COMPLETED, OrderEffect.Completed)
transition(OrderStatus.CANCELLED, OrderEffect.Cancelled)
}

state(OrderStatus.FULLY_FILLED) {
isMutable = true

onValidate { order ->
require(order.quantityFilled == order.quantity) { "Order must have quantity filled to the full order quantity to be set as FULLY_FILLED" }
}

onCommit { order ->
event.withDetails {
modifiedBy = event.userName
modifiedTime = now()
}
}

transition(OrderStatus.COMPLETED, OrderEffect.Completed)
transition(OrderStatus.CANCELLED, OrderEffect.Cancelled)
}

state(OrderStatus.COMPLETED) {
isMutable = false

onCommit { order ->
event.withDetails {
modifiedBy = event.userName
modifiedTime = now()
}
}
}

state(OrderStatus.CANCELLED) {
isMutable = false

onCommit { order ->
event.withDetails {
modifiedBy = event.userName
modifiedTime = now()
}
}
}
}
}

//When using the StateMachine API we also have SideEffects, which can be subscribed to and trigger actions if required.
sealed class OrderEffect {
object New : OrderEffect()
object Assigned : OrderEffect()
object PartiallyFilled : OrderEffect()
object FullyFilled : OrderEffect()
object Completed : OrderEffect()
object Cancelled : OrderEffect()
}

API

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

  • the entity of the State Machine. In our example, it is called Order.
  • the state of the entity field to manage; this field needs to be ENUM field. In our example, it is called OrderStatus.
  • the side effect of a state change, which is something you can subscribe to when the state changes to trigger further actions. In our example, it is called OrderEffect and you can see we add it in-line in the same file we create the StateMachine module.

The following is an example State Machine written in kotlin. This would be injected into your event handler as described in the configuration options section.

This option is only recommended when you are using the event handler API for your event handle.

@Module
class OrderStateMachine @Inject constructor(
db: AsyncEntityDb
) {
private val internalState: StateMachine<Order, OrderStatus, OrderEffect> = db.stateMachineBuilder {
...
}
}

sealed class OrderEffect {
object New : OrderEffect()
object Assigned : OrderEffect()
object PartiallyFilled : OrderEffect()
object FullyFilled : OrderEffect()
object Completed : OrderEffect()
object Cancelled : OrderEffect()
}

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
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)

Injecting State Machine

Event Handler API

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

Find more details on injection on our Custom Components page

GPAL Event Handler file

It is recommended to use in-line state machines as described in example configuration. However, if needed you can also inject into a GPAL event handler per the following example.

Where adopting this, you should use stateMachine handle instead of the entityDb, to commit database changes to ensure the state machine logic is triggered.

eventHandler {
val stateMachine = inject<OrderStateMachine>()

eventHandler<Order>(name = "ORDER_INSERT", transactional = true) {
permissioning {
permissionCodes = listOf("ORDERR")
auth(mapName = "ENTITY_VISIBILITY") {
authKey {
key(data.counterpartyId)
}
}
}
onValidate { event ->
val message = event.details
verify {
entityDb hasEntry Counterparty.ById(message.counterpartyId)
entityDb hasEntry Instrument.ById(message.instrumentId)
}
ack()
}
onCommit { event ->
val order = event.details
order.enteredBy = event.userName
stateMachine.insert(entityDb, order)
ack()
}
}
}