How to Mock a Cross-Shard Saga in Python
Simulate a distributed saga with compensating transactions across multiple database shards using a lightweight Python class that tracks executed steps and rolls them back in reverse on failure.
Python code
46 linesimport json
class SagaState:
def __init__(self, saga_id):
self.saga_id = saga_id
self.executed_steps = []
self.compensations = []
def execute_step(self, shard, step_name, operation):
self.executed_steps.append((shard, step_name))
print(f"[Saga {self.saga_id}] Executing step '{step_name}' on shard {shard}")
result = operation()
self.compensations.append((shard, step_name, result))
return result
def compensate(self):
print(f"[Saga {self.saga_id}] Compensating...")
for shard, step_name, result in reversed(self.compensations):
print(f" Compensating '{step_name}' on shard {shard}: undo {result}")
self.compensations.clear()
def create_user():
return {"user_id": 1, "status": "created"}
def create_balance():
return {"balance": 100}
def create_account():
return {"account_id": 10}
if __name__ == "__main__":
saga = SagaState("saga-123")
# Simulate cross-shard saga: user on shard 1, balance on shard 2, account on shard 1
saga.execute_step("shard-1", "create_user", create_user)
saga.execute_step("shard-2", "create_balance", create_balance)
saga.execute_step("shard-1", "create_account", create_account)
# Simulate failure mid-saga -> trigger compensation
saga.compensate()
print("Saga completed successfully (mock).")
Output
[Saga saga-123] Executing step 'create_user' on shard shard-1
[Saga saga-123] Executing step 'create_balance' on shard shard-2
[Saga saga-123] Executing step 'create_account' on shard shard-1
[Saga saga-123] Compensating...
Compensating 'create_account' on shard shard-1: undo {'account_id': 10}
Compensating 'create_balance' on shard shard-2: undo {'balance': 100}
Compensating 'create_user' on shard shard-1: undo {'user_id': 1, 'status': 'created'}
Saga completed successfully (mock).
How it works
The SagaState class records each executed step and its result in executed_steps and compensations. When compensate() is called, it iterates through compensations in reverse order — mirroring how real saga frameworks undo the last operation first. The reversed() call ensures LIFO rollback semantics, so shard writes are undone in the opposite order they were applied. Each operation is wrapped in execute_step, which stores the shard name alongside the result to simulate cross-shard coordination. The mock ignores actual database connections, making it ideal for testing saga orchestration logic in isolation.
Common mistakes
- Not clearing `compensations` after rollback, causing duplicate compensation on retries
- Executing compensations in forward order instead of `reversed()` to undo the latest step first
- Forgetting that `execute_step` must return the operation result for downstream logic
- Hardcoding shard names instead of passing them at runtime to make the mock flexible
Variations
- Extend `SagaState` with a `run()` method that auto-triggers compensation on any exception raised by an operation
- Use a decorator on operations to automatically record and compensate without explicit `execute_step` calls
Real-world use cases
- Validating orchestration logic in unit tests when multiple microservices write to different database shards fail mid-transaction.
- Prototyping a new saga-based checkout flow without spinning up actual sharded databases or network services.
- Teaching distributed-systems concepts like LIFO compensation by simulating multi-shard writes in a classroom exercise.
Sponsored
More from Database scaling & optimization
- Approximate Count with HyperLogLog in Python medium
- B-Tree Insert and In-Order Traversal in Python hard
- Broadcast a Small Reference Table in Python easy
- Build a Full Text Search Index in Python medium
- Build a Partial Index Mock in Python for Database Filtering easy
- Composite index leftmost prefix in Python medium
Keep learning
Related tutorials and quizzes for this topic.