Compensation — the pattern card
The four rules
- Only completed activities compensate.
- Handlers run in reverse completion order.
- Handlers are ordinary tasks — the retry/dead-letter pipeline applies to them.
- Triggering is explicit — a compensation throw event you draw, never automatic.
XML skeleton
<serviceTask id="reserveFunds" flowable:delegateExpression="${treasuryReserve}"/>
<boundaryEvent id="reserveComp" attachedToRef="reserveFunds">
<compensateEventDefinition/>
</boundaryEvent>
<serviceTask id="releaseFunds" isForCompensation="true"
flowable:delegateExpression="${treasuryRelease}"/>
<association associationDirection="One"
sourceRef="reserveComp" targetRef="releaseFunds"/>
<intermediateThrowEvent id="undoEverything">
<compensateEventDefinition/>
</intermediateThrowEvent>
Notes: the handler is marked isForCompensation="true" (it is unreachable by normal
sequence flow) and linked by an association, not a sequenceFlow.
Does this step need a handler? (checklist)
- [ ] Does the step commit an effect in an external system? (Engine-internal state needs no handler — rollback and normal flows cover it.)
- [ ] Can a later step abort the overall outcome? (If nothing after it can fail the whole, there is nothing to unwind for.)
- [ ] Is the undo mechanical — a cancel/release/void API with no approvals? (If the undo has its own workflow, model a subprocess on the failure path instead.)
- [ ] Is the undo idempotent? (Handlers retry; a double-release must be safe.)
- [ ] Could reordering remove the need? (Do the irreversible step last.)
Three or more boxes ticked → attach a handler. Otherwise simplify first.
Testing minimum
- One test per prefix: fail after step 1, after step 2, … and assert exactly the right handlers ran, in reverse order.
- One test where a handler fails → assert it dead-letters and the incident is visible (lesson 05’s triage finds it).
