Skip to main content

Workflow (State Machine)

Almost every application has workflows where work items move from one state to another, such as new, amended and completed. But it can be difficult to manage transitions from one state to another using a standard Event Handler:

  • You might want to limit transitions (e.g. you can only transition from new to amended, and never from amended to new).
  • You might need validation for specific states (e.g. to move to completed, the quantity must be greater than 0)

The State Machine provides easy ways of implementing these requirements.

For example, if you are building an Order Management System, you could define the following states for your orders:

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

To achieve this, you can create a table called ORDER_STATUS. Each state would have a field of type ENUM in the table.

Once you have the states, there are two main sets of logic that you have to define. The recommended way to do this is to create dedicated eventHandler code blocks in your Event Handler service:

  • Transitions. You must define all the required transitions from state to state. In our example, you could define that an order must be ASSIGNED before it is moved to PARTIALLY_FILLED. And you might want CANCELLED orders to remain cancelled, so you would not define any transitions from there.

  • Validation. You must define the checks that ensure that the trade or order has the right information to make the required transition. For example, an Order must have ASSIGNED_TO set to a valid user in the system to transition to ASSIGNED.

You need to create an eventHandler for every required transition in the workflow - no eventHandler, no transition.

Example configuration

You can specify your State Machine in a GPAL Event Handler. This is the recommended approach when using GPAL in your application.

info

If you decide to write your eventHandler codeblocks using the Event Handler API, you need to create your State Machine as a set of Java or Kotlin classes. Our section on the StateMachine API provides details and examples.

The following example of a State Machine defines events for insert and modify. After these, it defines five transitionEvent 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(OrderStatus.NEW, OrderStatus.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

The recommended way to create a State Machine is to write it in a GPAL Event Handler. Alternatively, you can write specific classes in Kotlin and inject them for use. Configuration options for both are detailed here.

stateMachine

This takes two parameters:

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

insertEvent

Defines an event on the State Machine for inserting the entity. The resulting eventHandler will be named EVENT_<table_entity_name>_INSERT. In the example below the event would be called EVENT_ORDER_INSERT.

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

This specifies the valid values that can be set on the tableField on an insertEvent

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

This 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

This validates the inbound event details. See the Event Handler onValidate documentation for the full usage of onValidate.

One difference from a regular eventHandler onValidate block is that there is no need to return an ack. This is returned implicitly where no exception is hit; otherwise, a nack is 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.

  • You don't have to write the main object to the DB. This also happens implicitly.
  • You don't have to return an ack. This is returned implicitly where no exception is hit; otherwise, a nack is 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 example below, 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 for a 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 blocks (which are the same as for 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 example below 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 blocks (which are the same as for 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 the 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, so the test configuration is the same as for Event Handlers. You should add further tests to test your transition workflow logic works as expected.

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 in this 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 State Machine 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 Handler.

@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 how to read state S from table T.

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 that you use in-line State Machines, as described in the example configuration. However, you can also inject into a GPAL Event Handler.

If you do this, use stateMachine (not entityDb) to commit database changes. This ensures that the State Machine logic is triggered.

Here is an example:

eventHandler {
val stateMachine = inject<OrderStateMachine>()

eventHandler<Order>(name = "ORDER_INSERT", transactional = true) {
permissioning {
permissionCodes = listOf("ORDER")
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()
}
}
}