Compensation
Sometimes the unexpected happens: for example, a customer changes his mind and wants to cancel an order or the stock are running low. Actually, in real world: the change is the only constant!
Workflow developers start defining the happy path, then they have to deal with unexpected results (out of stock, order cancellation events, etc). A workflow could define a long chain of activities and under certain conditions, it might be necessary to define an exit strategy: warning all parties involved that they must undo what has been done before.
When you have short running transactions (during milliseconds), you could aspire to the convenient atomic transaction, but as long as your tasks span over multiple systems this option brings so many drawbacks which make it unrealistic, especially when they run in a cloud context. In fact, in the microservices architecture, the Saga Pattern is a well know practice when dealing with a cancellation.
When you deal with long running transactions, as known as business transactions, that spans of over minutes, hours or days, the only option available is to revert what was done previously, this could have consequences: for example, Hotels usually applies a cancellation fee when the cancellation is too near to the reservation day.
A workflow might involve multiple logical steps, there are conditional paths that depends on specific instance data. When the business logic requires a cancellation of the workflow instance, you need to reverse the flow of actions, undoing what was previously done. In the workflow nomenclature, this capability is called Compensation.
The good news is that the Serverless Workflow engine helps you in this difficult situations! In fact, the workflow developer can define for each action a compensation action that will only be invoked if necessary. Behind the scenes, the workflow engine, when an action is completed, records the corresponding compensation action in a stack. If the workflow subsequently encounters conditions that require compensation, only then does the engine retrace the stack of compensation activities to close everything correctly.
In short, Serverless Workflow Compensation is very handy whenever dealing with cancellation!
Cancelling Event Sprint
In our use case, we have to deal with situations where customers have to cancel their orders. Specifically, if the cancellation event arrives before the shipping one, it is necessary to call for compensation: the concrete compensation action depends on the previous execution path, if the item was available, the compensation action will call the internal shipping department otherwise the compensation action will deal with the external supplier.
The following picture shows an high level design:

| Given the simplistic nature of the example, one might consider replacing compensation with a gateway followed by compensatory actions. However, it should be borne in mind that in an even slightly more complex flow, implementing compensation in imperative mode would make the implementation complex and error prone. |
Below, we will add the definition of the new event type CancelEventType and a forking that distinguishes between the two possible input events:
-
shippingEvent -
cancelEvent
Open the swf file:
-
Inside the event section add the following declaration:
{ "name": "cancelEvent", "kind": "consumed", "type": "CancelEventType", "source": "Client", "correlation": [ { "contextAttributeName": "orderid" } ] } -
In the state section, locate the
Order Shippedevent node and overwrite it with the following snippet:{ "name": "Order Shipped or Cancelled", "type": "event", "transition": "Is Shipped?", "exclusive": true, "onEvents": [ { "eventRefs": [ "shippingEvent" ] }, { "eventRefs": [ "cancelEvent" ], "eventDataFilter": { "data": "{cancel:true}" } } ] },The new event state definition is able to listen for two different events. At the cancellation event, we can see the statement
eventDataFilterthe effect of which is to introduce a new Boolean data:cancel:true. In such a way, we will later be able to know which event has arrived. -
Update the
transitionthe statesPrepare for ShippingandForward to External Supplierto point to the new nameOrder Shipped or Cancelled -
Add the following switch state after the
Order Shipped or Cancelledstate:{ "name": "Is Shipped?", "type": "switch", "dataConditions": [ { "name": "order cancelled", "condition": ".cancel == true", "transition": "Compensate Order" } ], "defaultCondition": { "transition": "Notify Customer" } }, -
After the previous state, Add an operation to handle the cancel event:
{ "name": "Compensate Order", "type": "operation", "actions": [ { "name": "printAction", "functionRef": { "refName": "printMessage", "arguments": { "message": "\"Compensate Order\"" } } } ], "end": { "terminate": true, "compensate": true } }, -
Locate the
Notify Customeroperation state and adjust theendproperty in this way:"end": { "terminate": true }This measure forces the workflow termination.
The following picture shows the result of the above procedure on the workflow diagram:
|
You can compare your resulting workflow with the expected solution at this stage in |
Test the cancelling event
In this section, we are going to test that the workflow is able to resume the execution when two types of CloudEvents arrives:
-
ShippingEventType -
CancelEventType
Then, depending on the incoming message, the workflow continues in one of the two corresponding branches.
Even if you trigger the compensation no compensation activity is defined, so at this stage of the exercise you can only look for the Compensate Order string in the logs.
|
Based on the previous test activities, you should be able to figure out how to test this scenario. Please, note that in the probe.http file, you already have the cancelEvent rest request.
Compensation Activities Sprint
In this section, you will add two compensation activities for Prepare for Shipping and Forward to External Supplier.
Afterward, you will check that the runtime will call them when compensation is triggered by the cancel event.
Compensation activities are operation states, they could be placed anywhere in the state array. However, we suggest to place the definition nearby the corresponding primary state so it’s easier to orientate.
|
-
Locate
Prepare for Shippingand append right afterward:{ "name": "Restore Inventory", "type": "operation", "usedForCompensation": true, "actions": [ { "name": "printAction", "functionRef": { "refName": "printMessage", "arguments": { "message": "\"Restore Inventory\"" } } } ] }, -
Inside
Prepare for Shippingadd the following property:"compensatedBy": "Restore Inventory",In the workflow diagram you should see the two states linked by a yellow dotted line:
-
Locate
Forward to External Supplierand append right afterward:{ "name": "Cancel Supplier Order", "type": "operation", "usedForCompensation": true, "actions": [ { "name": "printAction", "functionRef": { "refName": "printMessage", "arguments": { "message": "\"Cancel Supplier Order\"" } } } ] }, -
Inside
Forward to External Supplieradd the following property:"compensatedBy": "Cancel Supplier Order",
In the workflow diagram you should see the following situation:

| At the time of writing, the diagram rendering has a bug that shows overlapping lines, so the final outcome is slightly confusing. |
|
You can compare your resulting workflow with the expected solution at this stage in |
Testing the compensation
To test the compensation you have to send the following events:
-
New order (
OrderEventType) -
Cancel the order (
CancelEventType)
Then you can check if the compensation activity is triggered.
Unless, you changed it before, the rest call provided in probe.http contains the item id 1110 which is supposed to trigger the internal shipping path. So when you trigger the cancellation you should see in the log the following line:
Restore Inventory
Repeat the test using an item id with no 0 (e.g. 1234), upon cancellation you should see in the log the following line:
Cancel Supplier Order
CONGRATULATION!!! Your Serverless Workflow compensates wisely!