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.

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

Python code

58 lines
Python 3.9+
import 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

stdout
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

  1. Use `pydantic` instead of dataclasses for validation and schema enforcement of event payloads
  2. 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

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.