How to implement saga orchestration with compensating steps in Python

Orchestrate a distributed transaction across services, rolling back completed steps with compensations when a later step fails.

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

Python code

62 lines
Python 3.9+
class InventoryService:
    def reserve(self, order_id):
        print(f"[Inventory] Reserving stock for order {order_id}")
        return True

    def compensate(self, order_id):
        print(f"[Inventory] Releasing stock for order {order_id}")


class PaymentService:
    def charge(self, order_id):
        print(f"[Payment] Charging customer for order {order_id}")
        return True

    def compensate(self, order_id):
        print(f"[Payment] Refunding customer for order {order_id}")


class ShippingService:
    def ship(self, order_id):
        print(f"[Shipping] Shipping order {order_id}")
        return True

    def compensate(self, order_id):
        print(f"[Shipping] Canceling shipment for order {order_id}")


def place_order(order_id):
    inventory = InventoryService()
    payment = PaymentService()
    shipping = ShippingService()

    steps = [
        ("reserve inventory", lambda: inventory.reserve(order_id), lambda: inventory.compensate(order_id)),
        ("charge payment", lambda: payment.charge(order_id), lambda: payment.compensate(order_id)),
        ("ship order", lambda: shipping.ship(order_id), lambda: shipping.compensate(order_id)),
    ]

    executed = []

    for step_name, action, compensate in steps:
        print(f"Executing: {step_name}")
        if not action():
            print(f"FAILED at: {step_name}")
            print("Starting compensation...")
            for executed_name, executed_compensate in reversed(executed):
                print(f"Compensating: {executed_name}")
                executed_compensate()
            return False
        executed.append((step_name, compensate))

    print("Order placed successfully!")
    return True


if __name__ == "__main__":
    # Simulate a successful flow
    place_order("ORD-123")
    print("-" * 40)
    # Simulate a failure during payment
    PaymentService.charge = lambda self, order_id: False  # monkey-patch to fail
    place_order("ORD-456")

Output

stdout
Executing: reserve inventory
[Inventory] Reserving stock for order ORD-123
Executing: charge payment
[Payment] Charging customer for order ORD-123
Executing: ship order
[Shipping] Shipping order ORD-123
Order placed successfully!
----------------------------------------
Executing: reserve inventory
[Inventory] Reserving stock for order ORD-456
Executing: charge payment
FAILED at: charge payment
Starting compensation...
Compensating: reserve inventory
[Inventory] Releasing stock for order ORD-456

How it works

The saga pattern breaks a distributed transaction into discrete steps, each with a paired compensation action. The steps list stores (name, action, compensate) tuples, and the executed list tracks only completed steps so we know what to roll back. When a step returns False, the loop exits and replays compensations in reverse order (LIFO) to undo prior work. This mirrors real-world systems where services like inventory, payment, and shipping must stay consistent without a single database transaction. The mock services print their actions so you can see exactly when reservations, charges, and compensations happen.

Common mistakes

  • Compensating steps in forward order instead of reversed, which breaks dependencies
  • Forgetting to append the compensation function to `executed` before running the action
  • Not handling exceptions thrown inside `action()`, only checking boolean returns
  • Assuming compensation is always safe to run even if a step never fully completed

Variations

  1. Use a saga coordinator class to encapsulate step registration and rollback logic
  2. Async version with asyncio where each step awaits external service calls and retries on transient failures

Real-world use cases

  • Coordinating order fulfillment across inventory, payment, and shipping microservices when a payment fails and inventory must be released.
  • Rolling back a multi-step onboarding flow (create account, provision resources, send welcome email) when provisioning errors out.
  • Managing refund workflows in travel booking systems where hotel, flight, and car rental reservations each need cleanup.

Sponsored

Run this sample

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

Open editor

More from System design patterns

Related tutorials and quizzes for this topic.