Lesson 1 of 2
Sagas, and Why Compensation Is a Business Decision
A saga trades atomicity for availability: each step commits locally, and a failure later runs compensating actions. What "compensate" means is a question for the business.
A saga is a sequence of local transactions, each in one service, where each step has a compensating action that semantically undoes it. If step four fails, the saga runs the compensations for steps three, two and one in reverse. The concept predates microservices — Garcia-Molina and Salem described it in 1987 for long-running database transactions — and it is now the standard answer to cross-service consistency.
A booking saga, forward and compensating
Each step commits locally. Failure at any point runs the compensations for the completed steps in reverse.
Four forward steps: reserve seat, authorise payment, issue ticket, notify customer. Below them, three compensating steps: release seat, void authorisation, cancel ticket. A failure at the issue-ticket step triggers void authorisation followed by release seat.
Forward path
1. Reserve seat
2. Authorise payment
3. Issue ticket
4. Notify customer
Compensations
Release seat
undoes step 1
Void authorisation
undoes step 2
Cancel and refund
undoes step 3
None needed
step 4 is last
| Step | Compensation | Reversibility |
|---|---|---|
| Reserve inventory | Release the reservation | Clean — nothing external observed it |
| Authorise a payment | Void the authorisation | Clean if done promptly; the customer may see a pending amount |
| Capture a payment | Refund | Visible to the customer, costs fees, may take days to appear |
| Send a notification | Send a correction | Not reversible; the customer has already read it |
| Ship a physical item | Arrange a return | Expensive, slow, and requires the customer to act |
interface SagaStep {
readonly name: string;
execute(context: SagaContext): Promise<void>;
compensate(context: SagaContext): Promise<void>;
}
async function runSaga(steps: readonly SagaStep[], context: SagaContext): Promise<SagaOutcome> {
const completed: SagaStep[] = [];
for (const step of steps) {
try {
await step.execute(context);
completed.push(step);
// Persisted after each step: a crash here must not lose the knowledge
// that step 2 succeeded, or its compensation will never run.
await saveSagaState(context.sagaId, completed.map((s) => s.name));
} catch (error) {
for (const done of [...completed].reverse()) {
// Compensations retry independently and are idempotent; one failing
// compensation must not prevent the others from running.
await compensateWithRetry(done, context);
}
return { status: 'compensated', failedAt: step.name };
}
}
return { status: 'completed' };
}In practice
Reordering a saga instead of improving it
An online pharmacy ran a saga for prescription orders: verify prescription, charge card, dispatch from the warehouse, notify the patient. Roughly fifteen times a week the dispatch step failed after the charge, and the compensation was a refund plus an apology.
Constraints
- Dispatch fails when stock is miscounted or a batch is quarantined
- Refunds take three to five working days to reach the customer
- Patients frequently reordered elsewhere while waiting
- Regulatory rules prevent dispatching without a verified prescription
Decision
Reorder the saga: verify prescription, reserve the specific stock batch, authorise (not capture) the payment, dispatch, then capture the payment and notify. Failures now occur before any money moves.
Why
Nothing about the compensation logic was wrong; the ordering was. Charging before confirming that the item could actually be dispatched meant the most expensive compensation ran fifteen times a week for a condition that could have been detected first. Reserving the batch made the dispatch failure detectable before payment rather than after.
What it cost
Authorisations now sit open for up to two hours during warehouse picking, which some card issuers show to customers as a pending charge and which occasionally expires and must be re-authorised. That produces about two support contacts a week, against fifteen refunds and a meaningful number of lost customers before. The team also had to handle authorisation expiry, which is a new failure mode they did not previously have — accepted as clearly the cheaper problem.
Designing a compensating action
As a developer
Implements the technical reversal: delete the record, release the lock, call the provider's void endpoint. Correct as far as it goes, and it treats compensation as an engineering concern.
As an architect
Treats each compensation as a customer-visible business process and takes it to the product owner: what does the customer see, what do they have to do, what does it cost us, and is there a case where we should absorb the loss rather than compensate at all? For a low-value item, writing off the cost is frequently better business than a return process that costs more than the item — and that is a decision engineering should surface rather than make.
Your saga's third step sends a confirmation email. Step four fails. What is the compensation for step three?RevealHide
There is none in the technical sense, and pretending otherwise is where saga designs go wrong. The email exists in someone's inbox and has probably been read. The available responses are to send a clear correction that explains what happened and what you are doing about it, or — better — to move the email to the end of the saga so it is only sent once the operation has genuinely completed. That is the general principle applied: order steps so that the irreversible ones come last. If the email must go early for a business reason, then its content should reflect the actual state, saying "we have received your order" rather than "your order is confirmed", so no correction is needed when a later step fails. Changing the wording is often the cheapest fix available in this whole module.
Key takeaways
- A saga guarantees a consistent end state, not atomicity or isolation; intermediate states are visible.
- Compensation is semantic — a business action that makes things right — not a technical undo.
- Order steps by reversibility, with the hardest to undo last; this prevents more pain than any machinery.
- Persist saga progress after each step, or a crash loses the knowledge that a compensation is owed.
- Sometimes the right compensation is to absorb the loss, and that is a business decision to surface.