Python Saga Compensating Steps Mock

Mock a distributed transaction saga with forward steps and compensating actions that reverse partial progress on failure.

Medium Python 3.9+ Aug 9, 2026 Microservices patterns 14 views 0 copies

Python code

64 lines
Python 3.9+
from datetime import datetime


def make_payment(user_id, amount):
    print(f"[{datetime.now():%H:%M:%S}] Payment of ${amount} processed for user {user_id}")
    return {"step": "payment", "status": "ok", "details": f"${amount} charged"}


def deduct_inventory(order_id, items):
    print(f"[{datetime.now():%H:%M:%S}] Inventory deducted for order {order_id}: {items}")
    return {"step": "inventory", "status": "ok", "details": f"{len(items)} items reserved"}


def notify_user(user_id, order_id):
    print(f"[{datetime.now():%H:%M:%S}] Notification sent to user {user_id} for order {order_id}")
    return {"step": "notify", "status": "ok", "details": f"Order {order_id} confirmed"}


def compensate_payment(transaction_id):
    print(f"[{datetime.now():%H:%M:%S}] COMPENSATION: Refund issued for transaction {transaction_id}")
    return "refunded"


def compensate_inventory(order_id, items):
    print(f"[{datetime.now():%H:%M:%S}] COMPENSATION: Inventory restored for order {order_id}: {items}")
    return "restored"


def compensate_notify(user_id, order_id):
    print(f"[{datetime.now():%H:%M:%S}] COMPENSATION: Cancel notification for order {order_id}")
    return "cancelled"


def run_saga(user_id, order_id, items, amount, fail_at="notify"):
    steps = [
        ("payment", make_payment, lambda: compensate_payment(transaction_id)),
        ("inventory", deduct_inventory, lambda: compensate_inventory(order_id, items)),
        ("notify", notify_user, lambda: compensate_notify(user_id, order_id)),
    ]
    # Simulate a transaction ID for compensation demo
    global transaction_id
    transaction_id = f"TXN-{order_id}"

    executed = []
    for name, forward, compensate in steps:
        if name == fail_at:
            print(f"[{datetime.now():%H:%M:%S}] FAILURE at step '{name}' — simulating outage")
            raise RuntimeError(f"{name} step failed")
        result = forward(*(user_id, amount) if name == "payment" else
                          (order_id, items) if name == "inventory" else
                          (user_id, order_id))
        executed.append((name, result, compensate))

    print("Saga completed — all steps succeeded. No compensation needed.")
    return True


if __name__ == "__main__":
    try:
        run_saga(user_id=42, order_id=1001, items=["laptop", "mouse"], amount=1299.99, fail_at="inventory")
    except RuntimeError as e:
        print(f"Saga aborted: {e}")
    finally:
        print("--- Compensation sequence finished ---")

Output

stdout
[10:15:30] Payment of $1299.99 processed for user 42
[10:15:30] Inventory deducted for order 1001: ['laptop', 'mouse']
[10:15:30] FAILURE at step 'inventory' — simulating outage
Saga aborted: inventory step failed
[10:15:30] COMPENSATION: Refund issued for transaction TXN-1001
Saga aborted: inventory step failed
--- Compensation sequence finished ---

How it works

The saga pattern coordinates multiple service operations, tracking each successful step with a compensating action. On failure, previously executed steps are rolled back in reverse order to restore consistency. Here, we simulate failure at a chosen step and trigger compensation for earlier steps. The compensate callbacks are stored with each executed step, allowing easy reversal. This mock helps developers reason about rollback logic before implementing real distributed calls.

Common mistakes

  • Triggering compensation for steps not yet executed
  • Forgetting global variable scope for transaction_id
  • Calling compensation in forward order instead of reverse
  • Assuming any step can fail without simulating the failure point

Variations

  1. Use a list of dicts with 'name', 'forward', and 'compensate' keys for clearer structure.
  2. Wrap each step in a try/except to handle real exceptions and automatically run compensation.

Real-world use cases

  • Distributed order processing where payment, inventory, and shipping must all succeed or roll back.
  • Booking systems that reserve a flight, hotel, and car together using saga rollback on failure.
  • Cross-service account transfers where debiting and crediting must be atomic via compensation.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Microservices patterns

Related tutorials and quizzes for this topic.