← All postsEngineering

Implementing Saga Patterns for Distributed Workflow Orchestration

The Problem with Distributed Transactions

When automation workflows interact with multiple third-party SaaS platforms and internal microservices, traditional database-style transactions (ACID) are impossible. You cannot wrap a Stripe API request, a HubSpot cURL execution, and a local PostgreSQL insert into a single BEGIN and COMMIT block.

In a simple workflow, if step three of four fails, previous steps remain executed in external systems. An invoice might be generated in your accounting system even though the user account creation in your database threw a database connection timeout. Over time, these unhandled partial failures pollute system state, corrupt reporting data, and require manual engineering hours to trace and remediate.

To build resilient automation pipelines across decoupled APIs, backend engineers use the Saga Pattern. Instead of attempting atomic distributed transactions, a Saga decomposes a workflow into a series of local transactions. If a step fails, the Saga orchestrator sequentially triggers compensating transactions to roll back the side effects of previous steps.

Choreography vs. Orchestration Sagas

There are two primary ways to implement the Saga pattern: Choreography and Orchestration.

Choreography-Based Sagas

In a choreographed implementation, services publish events and listen to events from other services. There is no central controller. Service A completes its task and emits UserCreated. Service B hears UserCreated, provisions a license, and emits LicenseProvisioned. If Service C fails during billing, it emits BillingFailed, and Services A and B listen for this event to reverse their local state.

While choreography reduces central coupling, it quickly becomes unmaintainable in complex backend integrations. Workflow logic is scattered across independent codebases, tracing execution state requires complex distributed tracing setups, and understanding the full state machine requires reading multiple microservice implementations.

Orchestration-Based Sagas

In an orchestrator-driven architecture—whether implemented via dedicated engines like Temporal, workflow tools like n8n, or custom backend state machines—a single centralized controller manages the execution sequence. The orchestrator explicitly executes step $T_1$, verifies response conditions, proceeds to step $T_2$, and if $T_3$ returns an error, explicitly triggers compensating actions $C_2$ and $C_1$ in reverse order.

For API integrations and enterprise workflow automation, Orchestration is almost always the preferred approach. It provides centralized state visibility, deterministic error pathways, and easier auditing.

Designing Effective Compensating Actions

Compensating actions ($C_n$) are not simple undo operations. They are explicit business logic designed to mitigate the effect of an already executed step ($T_n$).

When designing compensating actions, you must follow three core technical principles:

  1. Idempotency: A compensating action must be safe to execute multiple times. If the engine retries $C_2$ due to a network glitch, the target API must return a success status or an expected 404 Not Found / 409 Conflict state without duplicating actions.
  2. Forward Recoverability: Some steps cannot be undone, such as sending an outbound email or triggering a webhook to an external vendor. In these scenarios, the compensating action must record an internal state flag (e.g., notification_status: canceled_after_send) or issue an explicit counter-action (e.g., sending a follow-up cancellation notice).
  3. Independent Payload Context: A compensating action must hold all execution parameters required for invocation without depending on the live output of a later failed step.
Executed Action ($T_n$) Compensating Action ($C_n$) Technical Failure Mode to Anticipate
Create Customer in Stripe Provision Customer as Deleted / Inactive Stripe API returns 404 if creation was never acknowledged
Reserve Inventory in Database Release Reserved Inventory Batch Race conditions on stock quantities during execution
Provision AWS IAM Role Detach Policies and Delete Role IAM propagation delay before deletion request

Implementing an Orchestrated Saga Protocol

To implement a Saga orchestrator safely, the control loop must maintain a persistent execution log tracking completed steps and pending rollback hooks.

Below is a conceptual JSON state tracking object used by a workflow orchestrator to manage rollback state dynamically during execution:

{
  "saga_id": "saga_usr_98421a",
  "status": "EXECUTING",
  "current_step": 3,
  "executed_steps": [
    {
      "step_name": "create_stripe_customer",
      "status": "SUCCESS",
      "compensation": {
        "endpoint": "/v1/stripe/customers/cus_N48f12",
        "method": "DELETE",
        "payload": {}
      }
    },
    {
      "step_name": "provision_okta_user",
      "status": "SUCCESS",
      "compensation": {
        "endpoint": "/v1/okta/users/usr_00192/lifecycle/deactivate",
        "method": "POST",
        "payload": { "sendEmail": false }
      }
    }
  ]
}

If Step 3 (assign_db_entitlements) throws an unhandled exception or returns an unexpected schema, the engine halts forward execution, sets status to COMPENSATING, and iterates backwards through executed_steps.

Pseudo-Code Implementation of the Saga Loop

class SagaOrchestrator:
    def __init__(self, steps):
        self.steps = steps  # List of Step objects containing execute() and compensate()
        self.completed_steps = []

    def run(self, context):
        for step in self.steps:
            try:
                result = step.execute(context)
                context.update(result)
                self.completed_steps.append(step)
            except Exception as error:
                self.log_failure(step, error)
                self.rollback(context)
                raise SagaExecutionFailedException(f"Saga failed at step {step.name}") from error

    def rollback(self, context):
        # Iterate backwards over completed steps
        for step in reversed(self.completed_steps):
            success = False
            retries = 3
            while not success and retries > 0:
                try:
                    step.compensate(context)
                    success = True
                except Exception as comp_error:
                    retries -= 1
                    if retries == 0:
                        self.flag_manual_intervention_required(step, comp_error, context)

Handling Rollback Failures and Edge Cases

What happens when a compensating action itself fails? If $C_2$ encounters a 502 Bad Gateway from an external service, the Saga cannot simply abort, or the system remains in a partially rolled-back state.

Exponential Backoff and Retries for Compensations

Compensating actions must be wrapped in aggressive retry policies using exponential backoff with jitter. Because compensating actions are non-blocking to the end-user (who has already received a transaction failure response), the orchestrator can afford to retry compensations over minutes or hours.

Dead-Letter Queue (DLQ) Fallbacks

If a compensating action exceeds its retry budget, the execution context must be pushed to a Dead-Letter Queue. This context must include:

  • The exact parameters passed to the failed compensation.
  • The response body and headers returned by the downstream API.
  • The root exception that triggered the Saga rollback originally.

Engineering teams can then inspect the DLQ, fix underlying network issues or auth tokens, and replay the compensating action directly without needing to reconstruct system state manually.

Operational Benefits for Engineering Operations

Adopting Orchestrated Sagas transforms unstable automation scripts into enterprise-grade integration pipelines:

  • Audit Traceability: Every state transition, successful API payload, and compensation call is recorded in a structured log format.
  • Reduced Data Pollution: Downstream SaaS environments remain free of orphan objects created by partial workflow failures.
  • SLA Reliability: Instead of panicking on transient microservice outages, workflows roll back predictably, allowing upstream systems to retry cleanly.

Building reliable workflow automation requires accepting that third-party APIs and microservices will fail. By shifting your backend integration architecture from linear execution to orchestrator-driven Sagas, you build systems that maintain data consistency across all services by design.

Have a process eating your team's time?

Book a free 45-minute scoping call — no obligation.

Book a call