Skip to content

Compensation

Stephan Pelikan edited this page Oct 11, 2026 · 3 revisions

Compensation undoes work which a workflow already finished. A booking which was made is cancelled again. A payment which went through is refunded. BPMN draws this as a handler activity next to the activity it undoes, and the model decides when the handler runs. VanillaBP adds nothing to it. The handler is an ordinary workflow task, and VanillaBP has no API for you to call.

  1. A compensation handler is an ordinary workflow task
    What the deployment does with it, and what your application owes it.
  2. What the handler is given
    The aggregate as it is now, and why there is no snapshot of what it was.
  3. The order is the engine's answer
    What the specification asks for, and why VanillaBP promises nothing about it.
  4. What VanillaBP does not bring
    The call which does not exist, and the check which does not run.

A compensation handler is an ordinary workflow task

In the model, a compensation handler is an activity like any other. It is marked as an activity which compensates, and it is attached to a compensation boundary event. Every adapter reads it as the service task it is. The deployment collects it with the other tasks of the process. The wiring validation asks for a @WorkflowTask method for it. And that method runs under the same execution contract as every other handler.

@WorkflowTask
public void cancelBooking(final Ride ride) {
    ride.setBookingCancelled(true);
}

Nothing in this method says that it compensates. The model says it, and the method is wired by the same convention as the rest. This also means: if a model draws a compensation handler and the application has no method for it, they do not start together. It is the same for any task which no method serves.

A handler which throws behaves like every handler. TaskException completes the activity with a BPMN error. Anything else rolls the transaction back, and the BPMS (business process management system) retries. If the BPMS is a remote one, a repeated delivery is answered from the delivery record. The handler does not run twice.

What the handler is given

The handler gets the workflow aggregate, loaded by its id at the moment the handler is delivered. This is the state the workflow has reached by then. It is not the state it had when the compensated activity finished.

BPMN sees this differently. The specification takes a snapshot of the activity's data when the activity completes, and hands that snapshot to the handler. VanillaBP has no snapshot, because it has no process data to take one of. The aggregate belongs to your application, and your application decides what stays in it. If the undoing needs a value the original activity worked with, keep that value in the aggregate. For example, write down the booking number when the booking is made. Later that number cancels it.

The order is the engine's answer

The specification is precise here. Default compensation runs the handlers in the reverse order of the activities which produced them. It also says exactly what reverse means for a sequence flow and for the instances of a multi-instance activity.

VanillaBP promises none of this. The order is whatever the engine underneath does, and the engines differ. We do not make a promise we cannot keep on every BPMS. Know two consequences before you model a long chain of compensations. First, the handlers may run at the same time and not one after the other. This makes them two writers on one aggregate. Second, if an undoing only works in one order, draw it in that order. Do not leave it to a single throw event which compensates everything at once. Your BPMS's adapter has a Deviations page which says what your BPMS does. The Camunda 8 one says what was measured against a cluster.

You do not have to find this out in production. A throw event which starts more than one handler leaves the workflow with one token per handler. If the workflow aggregate of that process has no version attribute, the startup report names the process, the throw event and the handlers it starts. Next to them it lists the same ways out as for a parallel gateway. A throw event which undoes a single activity is not named, because then the workflow holds one token. Camunda 7 is named anyway, although it starts the handlers one after the other. The reason: a handler which waits, for example a user task, keeps its token while the next handler starts. Your application then completes two such handlers in two transactions, and these may overlap.

What VanillaBP does not bring

ProcessService has no call which triggers a compensation. A compensation starts where the model starts it, at a compensation throw event. Your application takes part by serving the handler.

There is also no startup check which compares a model with compensation against the BPMS you chose. The BPMS itself refuses the constructs it cannot run. What it runs differently from other engines is on its Deviations page. Read that page before you rely on compensation. Compensation is the path a workflow takes on a bad day, and a bad day is a poor moment to find out.

Clone this wiki locally