Compensation Is Not Rollback Across Service Boundaries
A local database rollback can erase uncommitted writes before other transactions are allowed to depend on them. A compensating operation has a different shape. It runs after an earlier operation has committed, often after that result has become visible to other components.
That distinction changes the consistency model. Compensation does not restore a distributed system to a state in which the original action never occurred. It adds another state transition whose domain meaning offsets some consequence of the first one.
This is the central boundary in saga-style coordination. A sequence of local commits can be paired with compensating actions, but those actions are ordinary distributed work: they can race with later changes, fail independently, be retried, and encounter effects that cannot be reversed exactly.
A committed action has already entered history
Consider two services participating in an order flow. One service reserves inventory and commits. A second service later rejects payment. If the inventory service releases the reservation, the release is not a rollback of the original database transaction. It is a new transaction.
The observable history can be represented as:
T1 inventory: reserve item 42 -> commit
T2 another operation observes it
T3 payment: reject authorization -> commit
T4 inventory: release item 42 -> commitAt T2, the reservation exists. Any component permitted to observe that state can react to it. The later release cannot erase the fact that the reservation was visible during that interval.
A database rollback has stronger local semantics because the transaction manager controls visibility and commit. Before commit, it can discard tentative writes according to the database’s transaction model. Once independent services have committed separate transactions, no single local rollback primitive spans those commits unless the systems participate in a distributed transaction protocol that provides such coordination.
Calling compensation a rollback can therefore hide the property that matters most: intermediate committed states are part of the system’s history.
The inverse operation is a domain decision
Some state changes have an obvious-looking inverse. Reserving one unit can be paired with releasing one unit. Adding a pending hold can be paired with removing that hold. Even these pairs require a domain identity that connects the compensating action to the exact effect being offset.
A broad operation such as increment available_stock by 1 is not necessarily equivalent to releasing a specific reservation. Concurrent activity may have changed the same aggregate between the two operations. A compensation expressed only as arithmetic can modify state that belongs to another operation.
An explicit reservation identity gives the second transaction a narrower target:
reservation r-81: active
|
v
reservation r-81: releasedThe state transition refers to the same logical reservation rather than attempting to reconstruct an earlier aggregate value. This matters whenever intervening writes are valid and must remain intact.
The same principle applies outside inventory. Reversing a ledger entry is commonly represented by another entry linked to the original rather than by deleting committed history. Cancelling a shipment request may create a cancellation state rather than making the request disappear. The exact representation depends on the domain, but the engineering constraint is stable: compensation must preserve unrelated changes that occurred after the original commit.
Snapshot restoration can overwrite valid concurrent work
Suppose an account-like record has a value of 100 before operation A. Operation A changes it to 80 and commits. Operation B then changes it to 95 and commits.
A compensator that saved the old value and later writes 100 does not merely negate A; it also discards the effect of B.
initial 100
A commits 80
B commits 95
restore A 100 <- B is lostWhether arithmetic compensation is valid depends on the state model. If A represents an independent delta of -20 and the domain permits adding an inverse delta of +20, applying that inverse to 95 yields 115. That result may be correct for an additive model, but it would be incorrect if A represented setting an absolute value to 80.
Compensation therefore cannot be derived mechanically from before-and-after snapshots. The operation’s semantics determine which later states can be compensated safely and which require conflict detection or manual resolution.
Version checks can make this boundary explicit. A compensator may accept only states that still satisfy a known precondition. Failure of that precondition is not automatically an infrastructure error; it can mean that intervening domain activity has made the original compensation formula invalid.
Irreversible effects expose the limit directly
Not every committed effect has a meaningful inverse. A message already delivered to an external recipient cannot be made unread by a later transaction. A physical action may already have occurred. An external API may support cancellation only before a particular state transition. A generated identifier may have been exposed and referenced elsewhere.
A compensating design has to model these boundaries rather than promise atomic reversal that the participating systems do not provide.
Sometimes the available action is corrective instead of inverse. A second notification can supersede the first. A refund can offset a captured charge without making the original capture cease to exist. A new status can mark an earlier request as cancelled while retaining both transitions in the record.
These are observable sequences, not erased histories. Systems that expose audit records, event streams, or external side effects make that fact especially clear, but the property exists even when the intermediate states are not retained for long-term inspection.
Compensation itself needs duplicate semantics
A coordinator can lose contact after requesting compensation. The target service may have committed the compensating transaction while the acknowledgement was lost. Retrying blindly can apply the inverse twice if the operation is not duplicate-safe.
A compensation command can carry the identity of the effect it is intended to offset. The receiving service can then record that the specific effect has already been compensated and return the existing outcome on a duplicate request.
Conceptually:
compensate reservation r-81
if r-81 is active:
mark r-81 released
else if r-81 is already released:
return existing resultThe exact transaction depends on the storage model. The important property is that duplicate detection and the compensating state change share an atomic boundary when the service promises duplicate-safe behavior.
This does not make every compensation idempotent in the mathematical sense. It establishes application-level duplicate handling for a particular command identity. A second, distinct compensation request may still represent a different operation and require separate validation.
Ordering cannot be treated as an implementation detail
A saga often has dependencies between committed actions. If action B was valid only after action A, compensation commonly needs to respect that dependency. Reversing A while B remains active can temporarily or permanently violate an invariant that the forward ordering protected.
Reverse order is a common shape for dependent actions, but it is not a universal rule. Independent actions may be compensated concurrently. Some compensations depend on information produced later in the forward path. Others cannot run after a downstream irreversible transition.
The dependency graph, not the textual order of service calls, determines the valid compensation order.
This also affects failure handling. If compensation C2 succeeds and C1 fails, the coordinator is now managing a partially compensated state. Retrying C1 may be appropriate if its preconditions still hold. Re-executing C2 should not be required merely to recreate a procedural sequence.
Persistent coordination state is useful here because process memory alone cannot describe what has already committed after a coordinator restart. The required durability can live in several architectures, but the state must distinguish forward completion, compensation intent, compensation completion, and cases that require intervention if those distinctions matter to recovery.
The consistency contract belongs in the operation model
Compensation works best as an explicit part of a domain protocol rather than as a generic exception handler attached to remote calls. The protocol can state which committed effects are compensable, the identity used to target them, the preconditions for reversal, and the outcome when reversal is no longer valid.
That contract also clarifies what callers may observe. If intermediate states are externally visible, the system offers a different consistency model from an atomic multi-resource transaction. Both models can be appropriate under their respective constraints, but they are not interchangeable.
The useful mental model is append rather than erase. A forward operation commits a fact about state. A compensating operation commits another fact that changes the current state in response. Once distributed work is viewed in those terms, concurrency, retries, audit history, and irreversible effects stop looking like edge cases around rollback. They become part of the compensation semantics themselves.