Skip to main content

A lightweight implementation of the Saga pattern for managing distributed transactions in Python

Project description

Simple Saga

A lightweight implementation of the Saga pattern for managing distributed transactions in Python.

Overview

The Saga pattern breaks down distributed transactions into a series of local transactions, each with a compensating transaction that can undo the changes if a later step fails. This library provides a simple, type-safe implementation with support for both synchronous and asynchronous operations.

Features

  • Simple API - Easy to use with a fluent interface
  • 🔄 Automatic Compensation - Failed transactions are automatically rolled back
  • Sync & Async Support - Works with both synchronous and asynchronous functions
  • 🔒 Type Safe - Full type hints with mypy support
  • 🪶 Lightweight - Zero dependencies (uses only Python standard library)
  • 📚 Well Documented - Comprehensive docstrings and examples

Installation

pip install simple-saga

Or with Poetry:

poetry add simple-saga

Quick Start

import asyncio
from simple_saga import SimpleSaga

# Define your actions
def create_order(order_id: str) -> dict:
    print(f"Creating order: {order_id}")
    return {"order_id": order_id, "status": "created"}

def cancel_order(order_result: dict) -> None:
    print(f"Cancelling order: {order_result['order_id']}")

async def reserve_inventory(product_id: str) -> dict:
    print(f"Reserving inventory for: {product_id}")
    return {"product_id": product_id, "reserved": True}

async def release_inventory(inventory_result: dict) -> None:
    print(f"Releasing inventory for: {inventory_result['product_id']}")

def charge_payment(amount: float) -> dict:
    print(f"Charging payment: ${amount}")
    # Simulating a payment failure
    raise Exception("Payment failed")

def refund_payment(payment_result: dict) -> None:
    print("Refunding payment")

# Create and execute saga
async def main():
    saga = SimpleSaga()

    # Add steps with actions and compensations
    saga.add_step(
        action=create_order,
        compensation=cancel_order,
        action_args=("ORDER-123",)
    )

    saga.add_step(
        action=reserve_inventory,
        compensation=release_inventory,
        action_args=("PRODUCT-456",)
    )

    saga.add_step(
        action=charge_payment,
        compensation=refund_payment,
        action_args=(99.99,)
    )

    try:
        results = await saga.execute()
        print("✅ Saga completed successfully!")
        for result in results:
            print(f"  Step {result.step_index + 1}: {result.result}")
    except Exception as e:
        print(f"❌ Saga failed: {e}")
        print("All completed steps have been compensated.")

if __name__ == "__main__":
    asyncio.run(main())

Output

Creating order: ORDER-123
✓ Step 1 completed: create_order
Reserving inventory for: PRODUCT-456
✓ Step 2 completed: reserve_inventory
Charging payment: $99.99
✗ Error at step 3: Payment failed
🔄 Starting compensation...
Releasing inventory for: PRODUCT-456
✓ Compensated step 2: release_inventory
Cancelling order: ORDER-123
✓ Compensated step 1: cancel_order
❌ Saga failed: Payment failed
All completed steps have been compensated.

Advanced Usage

Passing Multiple Arguments

saga.add_step(
    action=create_user,
    compensation=delete_user,
    action_args=("john_doe",),
    action_kwargs={"email": "john@example.com", "age": 30}
)

Custom Compensation Arguments

By default, the action's result is passed to the compensation function. You can override this:

saga.add_step(
    action=allocate_resource,
    compensation=deallocate_resource,
    action_args=("resource-id",),
    compensation_args=("resource-id",),
    compensation_kwargs={"force": True}
)

Method Chaining

saga = (
    SimpleSaga()
    .add_step(action=step1, compensation=undo_step1)
    .add_step(action=step2, compensation=undo_step2)
    .add_step(action=step3, compensation=undo_step3)
)

results = await saga.execute()

Reusing Saga Definitions

saga = SimpleSaga()
saga.add_step(action=step1, compensation=undo_step1)
saga.add_step(action=step2, compensation=undo_step2)

# Execute multiple times
await saga.execute()  # First execution
await saga.execute()  # Second execution (automatically resets)

Logging Control

The library uses Python's standard logging module:

import logging

# Configure logging
logging.basicConfig(level=logging.INFO)

# Or disable saga logs
logging.getLogger("simple_saga").setLevel(logging.WARNING)

API Reference

SimpleSaga

Main class for defining and executing sagas.

add_step(action, compensation, *, action_args=(), action_kwargs=None, compensation_args=(), compensation_kwargs=None)

Add a step to the saga.

Parameters:

  • action: Function to execute (can be sync or async)
  • compensation: Function to compensate if this or later steps fail (can be sync or async)
  • action_args: Positional arguments for the action
  • action_kwargs: Keyword arguments for the action
  • compensation_args: Positional arguments for the compensation
  • compensation_kwargs: Keyword arguments for the compensation

Returns: Self (for method chaining)

async execute() -> list[StepResult]

Execute all steps in the saga. If any step fails, automatically runs compensation for all previously executed steps in reverse order.

Returns: List of StepResult objects

Raises: Re-raises the exception that caused the saga to fail

reset() -> None

Reset the saga state, clearing all executed steps. Called automatically by execute().

StepResult

Dataclass containing the result of a saga step execution.

Attributes:

  • step_index: int - The index of the step
  • step_name: str - The name of the action function
  • result: Any - The result returned by the action

SagaStep

Dataclass representing a single step in the saga with action and compensation.

Attributes:

  • action: Callable - The action function
  • compensation: Callable - The compensation function
  • action_args: tuple - Positional arguments for the action
  • action_kwargs: dict - Keyword arguments for the action
  • compensation_args: tuple - Positional arguments for the compensation
  • compensation_kwargs: dict - Keyword arguments for the compensation

Design Decisions

Why async execute()?

Even though the library supports synchronous functions, execute() is async to handle mixed sync/async steps uniformly. This allows you to:

  • Mix sync and async functions in the same saga
  • Use async patterns for I/O-bound operations
  • Keep the API simple and consistent

Compensation Behavior

  • Compensations run in reverse order (LIFO)
  • Compensation failures are logged but don't stop the chain
  • Each compensation receives the action's result by default
  • You can override compensation arguments if needed

Development

Setup

# Clone the repository
git clone https://github.com/yourusername/simple-saga.git
cd simple-saga

# Install dependencies
poetry install

# Run type checking
poetry run mypy simple_saga

# Run linting
poetry run ruff check simple_saga

Project Structure

simple-saga/
├── simple_saga/
│   ├── __init__.py      # Package exports
│   ├── saga.py          # Main SimpleSaga implementation
│   └── schema.py        # Data classes (StepResult, SagaStep)
├── pyproject.toml       # Project configuration
├── README.md            # This file
└── CLAUDE.md           # Development guide

License

MIT License - see LICENSE file for details

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Acknowledgments

This library implements the Saga pattern as described in:

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

simple_saga-0.0.1.tar.gz (6.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

simple_saga-0.0.1-py3-none-any.whl (7.5 kB view details)

Uploaded Python 3

File details

Details for the file simple_saga-0.0.1.tar.gz.

File metadata

  • Download URL: simple_saga-0.0.1.tar.gz
  • Upload date:
  • Size: 6.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for simple_saga-0.0.1.tar.gz
Algorithm Hash digest
SHA256 1f389c94f8f091ddcc3deeba2ab8d494bb497bcf044e36ff1dcde45ea4d786c2
MD5 d1532ce1456d9932c30f31d59660f766
BLAKE2b-256 ba8e074a420082098f5f18b00f8457a187fdc02ba6d43c04cdc289d65cc7cf78

See more details on using hashes here.

Provenance

The following attestation bundles were made for simple_saga-0.0.1.tar.gz:

Publisher: publish.yml on wakita181009/simple-saga

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file simple_saga-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: simple_saga-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 7.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for simple_saga-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2aebd776b0f82cbc624477d75b034923163ba4aed84a66ffff5dda285c91603c
MD5 dae1928fbb7ed3c962b2a63fc77fde57
BLAKE2b-256 0deeeb59f2f28a8e90c19e0d18ab63b0b40d0d3471016cc01ffdfd27fd20ccf7

See more details on using hashes here.

Provenance

The following attestation bundles were made for simple_saga-0.0.1-py3-none-any.whl:

Publisher: publish.yml on wakita181009/simple-saga

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page