-
Notifications
You must be signed in to change notification settings - Fork 44
Traditional State Machine Order Fulfilment
Consider a typical order fulfilment process:
We now show how the above state machine can be modelled using Flux primitives:
Current State | Task | Event raised |
---|---|---|
Order Created | Decide if it's COD/online payment | Payment Pending |
OR Order Confirmed | ||
Order Packed | Pack order & proceed to shipment | Package Ready |
Order Shipped | Route order to final destination. Proceed for final mile delivery |
Ready For Delivery |
OR Payment Pending |
Or, alternatively, this is how we can represent the above table in code
final StateDefinition orderCreated = new StateDefinition(
"order created", /* Name of the state */
Collections.<EventDefinition>emptySet(), /* No Dependencies */
new Task() { /* Task to be executed */
public Pair<Event, FluxError> execute(Event... events) {
/* if order is COD */
return new Pair<Event, FluxError>(new Event("Order Confirmed"),null);
/* if order is for online payment */
return new Pair<Event, FluxError>(new Event("Payment Pending"),null);
}
});
final StateDefinition orderPacked = new StateDefinition(
"order packed" /* Name of the state */,
/* Depends on the "Order Confirmed" event. The event can be raised by Order Created OR after Payment Received */
Collections.singleton(new EventDefinition("Order Confirmed")) ,
new Task() {
public Pair<Event, FluxError> execute(Event... events) {
/* Work related to packing order */
return new Pair<Event, FluxError>(new Event("Package Ready"),null);
}
}
);
final StateDefinition orderShipped = new StateDefinition(
"order shipped"/* Name of the state */,
Collections.singleton(new EventDefinition("Package Ready")) /* Depends on the "Package Ready" event */,
new Task() {
public Pair<Event, FluxError> execute(Event... events) {
/* if order is COD */
return new Pair<Event, FluxError>(new Event("Payment Pending",null));
/* if order is Already paid for */
return new Pair<Event, FluxError>(new Event("Ready for Delivery",null));
}
}
);
final StateMachineDefinition fulfilment = new StateMachineDefinition(
"Simple Order Fulfilment", "Fulfilment", /* Description & Name */
Arrays.asList(orderCreated, orderShipped, orderPacked),
"order created" /* Start State */);
We can begin the execution of the state machine using WorkflowExecutionController
workflowExecutionController.init(fulfilment);
Once the state machine is initialised, Flux would begin processing the state(s) that do not have any dependencies.
For the example given above, this would be the "order created" state.
The above code illustrates how Flux would execute a state machine. We have made it a lot simpler for developers to write the same State Machine as something very similar to regular Java code.
Firstly, we can annotate any public method with the @Workflow
annotation to indicate that it defines the state transitions
public class SimpleOrderFulfilmentWorkflow {
@Workflow(version = 1)
public void start(OrderData orderData) {
final Promise<CreatedOrder> createdOrder = orderManager.create(orderData);
/* Now, we are in the Order Created state of the state machine */
Promise<ConfirmedOrder> confirmedOrder = processPaymentIfRequired(createdOrder);
/* With a confirmed order, we can now proceed for packing */
/* Warehouse service can pack a confirmed order
Notice how we are staying true to the state transition diagram above by explicitly defining confirmedOrder as a dependency before beginning order packing
*/
final Promise<PackedOrder> readyPackage = warehouseService.packOrder(confirmedOrder);
/* Shipment service can ship a ready Package */
final Promise<OrderDeliveryInformation> orderDeliveryInformation = shipmentService.ship(readyPackage);
}
}
This looks like vanilla java code, except for the presence of the Promise<V>
variables.
Also, each of the methods above - orderManager.create()
, processPaymentIfRequired()
, warehouseService.packOrder()
and shipmentService.ship()
are annotated with Flux's @Task
annotation.
What this means is that when the start() method [annotated with @Workflow
] is invoked, it basically "submits" the execution flow to the Flux engine.
The Flux engine, then, orchestrates the the state transitions by asynchronously invoking the actual code. Any dependency between two invocations is specified using Promise<V>
variables.
Promise<V>
variables can be thought of carrying the promise of the corresponding event being raised. Once a method executes successfully, the returned value is received as an Event<>
by the Flux engine.
Once the event is actually raised, the method (or, more formally, the task associated with a particular state) can be entered.
By specifying a signature like warehouseService.packOrder(Promise<ConfirmedOrder>)
, it is understood that the packOrder()
method can be invoked only once the Promise of a confirmedOrder is fulfilled.
In the above example, the promise of the ConfirmedOrder
is fulfilled by the processPaymentIfRequired()
method
@Task(version = 1, timeout = 1000l)
public Promise<ConfirmedOrder> processPaymentIfRequired(Promise<CreatedOrder> createdOrderPromise) {
/* Given that we are in the "Order Created" state of the state machine, we decide on how to proceed */
final CreatedOrder createdOrder = createdOrderPromise.get();
if (createdOrder.isCod()){
/* The call to paymentService.processOnlinePayment() is equivalent to raising the "Payment Pending" event. */
return orderManager.confirmOrder(createdOrderPromise,paymentService.processOnlinePayment(createdOrderPromise));
} else {
return orderManager.confirmOrder(createdOrderPromise);
}
}
The complete set of examples on how to define State Machines or Workflows is located in the examples module of the flux code base.