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 toPARTIALLY_FILLED
. And you might wantCANCELLED
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 toASSIGNED
.
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.
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:
tableField
is the field in the table that holds the state.transactional
is an optional boolean parameter (default is true). This makes the eventtransactional
in the same way as a standard eventHandler.
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, anack
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:
excludedFields
onEvent
: There is an exception on themodifyEvent
, per the example configuration. Because the object is already in the DB and we know the event uses the table object, both the event and the retrieved record are available inonEvent
.onValidate
onCommit
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:
excludedFields
onEvent
: Thre is an exception on thetransitionEvent
, per the example configuration. Because the object is already in the DB and we know the event uses the table object, both the event and the retrieved record are available inonEvent
.onValidate
onCommit
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
Signature | Description | |
---|---|---|
isMutable | var isMutable: Boolean | When set to false , any modification to the entity while in this state will be rejected |
initialState | fun initialState(sideEffect: E, transitionBuilder: TransitionBuilderInit<T>? = null) | The State Machine will accept the specified state on creation |
initialState | fun 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 |
initialStateWithContext | fun <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. |
initialStateWithContext | fun <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. |
transition | fun transition(newState: S,sideEffect: E,transitionBuilder: TransitionBuilderInit<T>? = null,) | The State Machine will transition from the current state to [newState] |
transition | fun 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 |
transitionWithContext | fun <C : Any> transitionWithContext(newState: S,sideEffect: E,transitionBuilder: ContextTransitionBuilder<T, C>.() -> Unit,) | The State Machine will transition from the current state to [newState] |
transitionWithContext | fun <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:
Signature | Description | |
---|---|---|
sideEffects | val sideEffects: Set<E> | Provides all possible side effects |
subscribe | fun subscribe(): Flow<Transition<T, S, E>> | Subscribers will be notified of state transitions |
highestPriorityTransition | suspend fun highestPriorityTransition(value: T,): ValidationResult<S>? | Given the table name, this gives the highest priority transition of the table if present or null |
currentState | suspend fun currentState(value: T): S? | Reads the current state [S] of [value] [T] from the database |
create | suspend fun create(value: T): Transition<T, S, E> | Creates a new entity in the database. |
update | suspend 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 |
update | suspend 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. |
updateIfExists | suspend 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 |
validate | suspend fun validate(value: T,newState: S,): ValidationResult<S> | Validates if [value] can successfully transition to [newState] |
withTransaction | suspend fun <O : Any?> withTransaction(transaction: AsyncMultiEntityReadWriteGenericSupport,handling: suspend StateMachine<T, S, E>.() -> O,): O | Will take a transaction and will handle all State Machine calls within [handling] within the provided transaction |
withTransaction | suspend fun <O : Any?> withTransaction(handling: suspend StateMachine<T, S, E>.() -> O,): O | Will 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()
}
}
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()
}
}
}