How to Mock a Choreography Saga in Python
Simulate a choreography-based saga with event envelopes, status tracking, and compensating actions to model distributed transactions.
Python code
58 linesimport json
from dataclasses import dataclass, asdict
from typing import List, Optional
from enum import Enum
class SagaStatus(Enum):
PENDING = "PENDING"
COMPLETING = "COMPLETING"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
@dataclass
class EventEnvelope:
event_type: str
order_id: str
status: SagaStatus
metadata: Optional[dict] = None
def to_json(self) -> str:
return json.dumps(asdict(self))
class ChoreographySagaMock:
def __init__(self):
self.events: List[EventEnvelope] = []
self._status: SagaStatus = SagaStatus.PENDING
def publish(self, event: EventEnvelope) -> None:
"""Record and print a published event, updating saga status."""
self.events.append(event)
if event.status in (SagaStatus.COMPLETING, SagaStatus.COMPLETED):
self._status = event.status
elif event.status == SagaStatus.FAILED:
self._status = SagaStatus.FAILED
print(f"Event: {event.to_json()}")
def run(self) -> None:
"""Simulate a full saga choreography with compensating actions."""
order_id = "ORD-12345"
self.publish(EventEnvelope("ORDER_CREATED", order_id, SagaStatus.PENDING))
self.publish(EventEnvelope("INVENTORY_RESERVED", order_id, SagaStatus.COMPLETING))
self.publish(EventEnvelope("PAYMENT_CHARGED", order_id, SagaStatus.COMPLETING))
# Simulate a failure mid-saga
if self._status != SagaStatus.COMPLETED:
self.publish(EventEnvelope("PAYMENT_FAILED", order_id, SagaStatus.FAILED))
self.publish(EventEnvelope("INVENTORY_RELEASED", order_id, SagaStatus.PENDING))
self.publish(EventEnvelope("ORDER_CANCELLED", order_id, SagaStatus.PENDING, {"reason": "payment error"}))
print(f"Final status: {self._status.value}")
print(f"Total events: {len(self.events)}")
if __name__ == "__main__":
saga = ChoreographySagaMock()
saga.run()
Output
Event: {"event_type": "ORDER_CREATED", "order_id": "ORD-12345", "status": "PENDING", "metadata": null}
Event: {"event_type": "INVENTORY_RESERVED", "order_id": "ORD-12345", "status": "COMPLETING", "metadata": null}
Event: {"event_type": "PAYMENT_CHARGED", "order_id": "ORD-12345", "status": "COMPLETING", "metadata": null}
Event: {"event_type": "PAYMENT_FAILED", "order_id": "ORD-12345", "status": "FAILED", "metadata": null}
Event: {"event_type": "INVENTORY_RELEASED", "order_id": "ORD-12345", "status": "PENDING", "metadata": null}
Event: {"event_type": "ORDER_CANCELLED", "order_id": "ORD-12345", "status": "PENDING", "metadata": {"reason": "payment error"}}
Final status: FAILED
Total events: 6
How it works
The ChoreographySagaMock class uses an in-memory list to record every event published during the saga, mimicking a message bus. The EventEnvelope dataclass wraps event type, order ID, status, and optional metadata, with to_json() serializing it for logging or transport. The publish() method appends events, updates the saga status based on event type, and prints each event as JSON. In run(), we simulate a normal flow with order creation, inventory reservation, and payment, then force a failure to demonstrate compensating actions (inventory release, order cancellation). This pattern models how choreography sagas rely on event-driven coordination with no central orchestrator, using status transitions to track overall transaction outcome.
Common mistakes
- Forgetting to update saga status on every event publish, leading to incorrect final state
- Hard-coding event types as strings instead of using an Enum or constants, causing typos
- Assuming events are delivered in order when in real distributed systems they may arrive out of order
Variations
- Use `pydantic` instead of dataclasses for validation and schema enforcement of event payloads
- Implement a `SagaCoordinator` class that listens for events and routes them to appropriate handlers
Real-world use cases
- Testing event-driven order processing flows in a microservices environment without spinning up message brokers.
- Creating local simulation harnesses for payment and inventory systems to validate saga logic during development.
- Generating fixtures for integration tests that verify compensating actions trigger correctly on failures.
Sponsored
More from Microservices patterns
- BFF aggregation pattern: combine multiple service responses in Python easy
- Backward Compatible Schema Evolution in Python medium
- Bulkhead Thread Pool per Service Mock in Python medium
- CQRS with Separate Read and Write Repositories in Python medium
- Cache-Aside Pattern in Python: Per-Service Mock easy
- Consumer Driven Contract Pact Mock in Python medium
Keep learning
Related tutorials and quizzes for this topic.