awesome-everything RU
↑ Back to the climb

Distributed Systems

Sagas: code reading

Crux Read real saga snippets — a compensating action, an orchestrator step, and a retried handler — predict the failure, and pick the highest-leverage fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min

Saga bugs do not look like saga bugs in code — they look like an ordinary handler that double-charges under retry, or a compensation that assumes a rollback it never gets. Read each snippet and pick the fix a senior would make before shipping.

Goal

Practise the read you do in every saga review: spot the missing idempotency key, the compensation that is really a rollback in disguise, and the orchestrator step that loses the in-flight workflow on a crash.

Snippet 1 — the compensating action

// T2 already committed: the card was charged and a payment row written.
// C2 is supposed to undo it after a later step fails.
func compensateCharge(ctx context.Context, orderID string) error {
    // delete the payment row so it looks like the charge never happened
    return db.Exec(ctx, "DELETE FROM payments WHERE order_id = $1", orderID)
}
Quiz

Why is this compensation wrong, and what should it do instead?

Snippet 2 — the orchestrator step

# Orchestrator drives the saga forward, step by step, in memory.
def run_order_saga(order):
    book_flight(order)          # T1
    book_hotel(order)           # T2
    try:
        charge_card(order)      # T3
    except StepFailed:
        cancel_hotel(order)     # C2
        cancel_flight(order)    # C1
    # progress lives only in this call frame
Quiz

The orchestrator process crashes right after book_hotel returns but before charge_card. What is the failure, and what fixes it?

Snippet 3 — the idempotent step

// Message delivery is at-least-once, so this handler can be invoked
// more than once for the same saga step.
func handleChargeCard(msg ChargeMsg) error {
    amount := msg.Amount
    if err := gateway.Charge(msg.CustomerID, amount); err != nil {
        return err
    }
    return savePaymentRow(msg.OrderID, amount)
}
Quiz

With at-least-once delivery this handler has a real-money bug. What is it, and what is the minimal fix?

Snippet 4 — the semantic lock

-- Saga A starts working an order. Saga B may touch the same order
-- concurrently because nothing is locked across saga steps.
UPDATE orders SET status = 'PENDING_PAYMENT' WHERE id = $1;
-- ... later steps of saga A run, then on success ...
UPDATE orders SET status = 'CONFIRMED' WHERE id = $1;
Quiz

What is the PENDING_PAYMENT status doing here, and what must other sagas do to make it work?

Recap

Every saga defect in this set is a known shape: a compensation that deletes a local record instead of issuing a real reversing action; an orchestrator that keeps progress only in memory and loses the workflow on a crash; a step that double-charges because at-least-once delivery met a non-idempotent handler; and a status field that only works as a lock if every saga honours it. Read for the missing idempotency key, the rollback-in-disguise compensation, and the unpersisted step — that is where saga bugs actually live.

Continue the climb ↑Sagas: build a fault-tolerant order saga
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.