Saga compensation: undo completed steps on failure

saga
Mechanism lab

Saga compensation: undo completed steps on failure

A multi-step order workflow fails at the charge step but leaves the stock reservation from step 1 in place. Add reverse-order compensation so a mid-flight failure undoes everything already done.

You'll predict the failure first, then write the fix and watch your own code drive the behavior.

Understand it step by step (한국어로 이해 → 영어로 말하기)

주문 한 건이 여러 단계로 나뉜다: 재고 잡기 → 카드 결제 → 배송. 중간에 한 단계가 실패하면, 이미 끝낸 앞 단계들을 도로 되돌려야 한다.
  1. 1

    주문 하나가 한 번에 끝나지 않고 세 단계로 나뉜다. 먼저 재고를 잡고, 그다음 카드로 결제하고, 마지막에 배송한다. 끝낸 단계는 'done' 목록에 하나씩 적어둔다.

    // step 1
    int stock = inventory.getOrDefault(item, 0);
    if (stock <= 0) return false;
    inventory.put(item, stock - 1);
    done.add("reserve");
    // step 2 charge, step 3 ship follow in order
    🗣 영어로 말해

    An order runs three steps in order: reserve stock, charge the card, then ship.

    checking microphone…

  2. 2

    문제는 중간에 실패할 때다. 재고는 잡았는데 지갑에 돈이 모자라서 결제가 실패한다. 그러면 주문은 안 됐는데 재고만 묶여 있다.

    ⚖️ Trade-off: 그냥 false만 돌려주고 끝내면, 잡아둔 재고가 영원히 묶인다. 주문도 없는데 물건은 못 판다.

    🔧 도구:반환값 false (그냥 멈추기 — 여기선 X)

    // charge fails but step 1 already mutated inventory
    int bal = wallet.getOrDefault(user, 0);
    if (bal < price) {
        return false; // BUG: 'reserve' in done, stock stuck at 0
    }
    🗣 영어로 말해

    If the charge fails, step one already reserved stock, so the item is stuck with no order.

    checking microphone…

  3. 3

    왜 DB 트랜잭션처럼 그냥 롤백하면 안 되나? 단계들이 서로 다른 시스템(재고, 결제, 배송)이라 하나의 트랜잭션으로 묶을 수가 없다. 자동 되돌리기가 없다.

    ⚖️ Trade-off: DB 한 곳이면 COMMIT/ROLLBACK 한 줄로 끝나는데, 시스템이 여러 개라 그게 안 된다.

    🔧 도구:DB 트랜잭션 ROLLBACK (한 DB 전용 — 여기선 X)분산 트랜잭션 2PC (느리고 무거움 — 보통 X)

    🗣 영어로 말해

    There's no automatic rollback here because each step lives in a different system.

    checking microphone…

  4. 4

    해결: 각 단계마다 '되돌리는 짝꿍 동작'을 미리 하나씩 만든다. 재고 잡기의 짝꿍은 재고 풀기, 결제의 짝꿍은 환불. 이게 보상(compensation) 동작이다.

    ⚖️ Trade-off: 단계마다 되돌리는 코드를 손으로 직접 다 짜야 한다. 공짜로 안 생긴다.

    ✅ Fix: 어차피 단계가 몇 개 안 되니, 짝꿍 하나씩 만드는 건 감당 가능. 이게 saga의 핵심.

    🔧 도구:Saga 패턴보상 트랜잭션(compensating action)

    // each forward step has a matching inverse (compensation)
    Map<String, Runnable> compensate = Map.of(
        "reserve", () -> inventory.merge(item, 1, Integer::sum),
        "charge",  () -> wallet.merge(user, price, Integer::sum)
    );
    🗣 영어로 말해

    So for each step I write a matching undo action — the compensation.

    checking microphone…

  5. 5

    되돌리는 순서가 중요하다. 끝낸 순서의 '거꾸로', 즉 가장 최근에 한 것부터 되돌린다. done 목록을 뒤에서부터 읽으면서 짝꿍 동작을 실행한다.

    ⚖️ Trade-off: 순서를 거꾸로 안 하면, 아직 안 일어난 일을 되돌리려다 꼬일 수 있다.

    ✅ Fix: done 목록을 역순으로 돌면 끝. 결제 실패 때는 재고 풀기만 실행돼서 깨끗해진다.

    🔧 도구:done 로그 역순 순회reverse-order compensation

    // undo completed steps newest-first from the done log
    for (int i = done.size() - 1; i >= 0; i--) {
        compensate.get(done.get(i)).run();
    }
    done.clear();
    return false;
    🗣 영어로 말해

    I undo the completed steps in reverse order, newest first, from the done log.

    checking microphone…

  6. 6

    마지막 함정: 되돌리는 동작 자체도 실패할 수 있다. 예를 들어 환불을 누르는 순간 네트워크가 끊긴다. 그래서 보상은 여러 번 눌러도 안전해야 하고, 실패하면 다시 시도할 수 있어야 한다.

    ⚖️ Trade-off: 보상이 실패하면 시스템이 어중간한 상태로 남는다. 그냥 두면 더 위험.

    ✅ Fix: 보상 동작을 멱등하게(같은 걸 두 번 해도 결과 같게) 만들고, 실패하면 재시도하게 한다.

    🔧 도구:멱등 보상(idempotent compensation)재시도(retry)데드레터/수동 개입

    // idempotent + retryable compensation (set, don't blind-add)
    for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
        try { inventory.put(item, RESERVED_STOCK); break; }
        catch (Exception e) { /* retry */ }
    }
    🗣 영어로 말해

    Compensation can fail too, so each undo must be idempotent and retryable.

    checking microphone…

6단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.

Explain it in English

Say it out loud in your own words — hit these terms — then check against the gold answer.

Say it in your own words — but hit these: saga · compensation · distributed transactions · rollback · java

checking microphone…