---
# === IDENTITY ===
id: business/erp-integration/saga-pattern-erp-transactions/2026
canonical_question: "What is the Saga pattern for distributed transactions across ERPs - orchestration vs choreography?"
aliases:
  - "How do I implement the Saga pattern for cross-ERP transactions?"
  - "Orchestration vs choreography for distributed ERP transactions"
  - "Compensating transactions in multi-ERP integration"
  - "Should I use saga orchestration or choreography for ERP-to-ERP flows?"
entity_type: erp_integration
domain: business > erp-integration > saga-pattern-erp-transactions
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Cross-ERP (Pattern-level)"
    vendor: "N/A — architecture pattern"
    version: "N/A"
    edition: "All"
    deployment: "cloud"
    api_surface: "REST, Event Bus, Message Queue"
  - name: "AWS Step Functions"
    vendor: "Amazon Web Services"
    version: "2024"
    edition: "Standard & Express"
    deployment: "cloud"
    api_surface: "REST"
  - name: "Temporal"
    vendor: "Temporal Technologies"
    version: "1.x"
    edition: "Cloud & Self-hosted"
    deployment: "cloud"
    api_surface: "SDK (Go, Java, Python, TypeScript)"

# === VERIFICATION ===
last_verified: 2026-03-02
confidence: 0.92
version: 1.0
first_published: 2026-03-02

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: null
  next_review: 2026-08-29
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Saga does NOT provide ACID isolation — concurrent sagas can read uncommitted data from other sagas (dirty reads)"
  - "Compensating transactions must be idempotent and retryable — partial compensation leaves the system in an inconsistent state"
  - "Compensating transactions are semantic inverses, not true rollbacks — a credit reversal is not the same as 'never charged'"
  - "Orchestrator is a single point of failure unless deployed with high availability (multi-AZ, active-passive)"
  - "Choreography with >5 participants becomes nearly impossible to debug without distributed tracing"
  - "ERP API rate limits constrain saga throughput — a 10-step saga across 3 ERPs may consume 30+ API calls per business transaction"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs strong ACID consistency within a single ERP"
    use_instead: "Use the ERP's native transaction support (e.g., SAP LUW, Oracle DB transactions)"
  - condition: "User needs a specific ERP's API capabilities"
    use_instead: "business/erp-integration/{system}-rest-api/2026"
  - condition: "Integration involves only 2 systems with simple request-reply"
    use_instead: "Direct API call with retry + dead letter queue — saga is overkill"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (individual records, <1s latency)"
      - "batch/bulk (scheduled, high volume)"
      - "event-driven (webhook, CDC, platform events)"
      - "long-running multi-step business process"
  - key: participant_count
    question: "How many systems participate in the distributed transaction?"
    type: choice
    options:
      - "2 systems"
      - "3-5 systems"
      - "> 5 systems"
  - key: consistency_requirement
    question: "What consistency model is acceptable?"
    type: choice
    options:
      - "Strong consistency (ACID) — all-or-nothing"
      - "Eventual consistency — acceptable if compensations handle failures"
      - "Best-effort — some data loss tolerable"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/saga-pattern-erp-transactions/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-02)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/event-driven-erp-integration/2026"
      label: "Event-driven ERP integration patterns"
  solves:
    - id: "business/erp-integration/order-to-cash-erp-integration/2026"
      label: "Order-to-cash cross-ERP integration playbook"
    - id: "business/erp-integration/procure-to-pay-erp-integration/2026"
      label: "Procure-to-pay cross-ERP integration playbook"
  alternative_to:
    - id: "business/erp-integration/two-phase-commit-erp/2026"
      label: "Two-phase commit (2PC) for ERP transactions"
  often_confused_with:
    - id: "business/erp-integration/event-sourcing-erp/2026"
      label: "Event sourcing vs saga — different patterns solving different problems"

# === SOURCES ===
sources:
  - id: src1
    title: "Pattern: Saga"
    author: Chris Richardson
    url: https://microservices.io/patterns/data/saga.html
    type: technical_blog
    published: 2023-01-15
    reliability: authoritative
  - id: src2
    title: "Saga distributed transactions pattern — Azure Architecture Center"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga
    type: official_docs
    published: 2025-02-25
    reliability: authoritative
  - id: src3
    title: "Saga orchestration pattern — AWS Prescriptive Guidance"
    author: Amazon Web Services
    url: https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/saga-orchestration.html
    type: official_docs
    published: 2024-11-01
    reliability: authoritative
  - id: src4
    title: "Mastering Saga Patterns for Distributed Transactions in Microservices"
    author: Temporal Technologies
    url: https://temporal.io/blog/mastering-saga-patterns-for-distributed-transactions-in-microservices
    type: technical_blog
    published: 2025-06-01
    reliability: high
  - id: src5
    title: "Saga Pattern Demystified: Orchestration vs Choreography"
    author: ByteByteGo
    url: https://blog.bytebytego.com/p/saga-pattern-demystified-orchestration
    type: technical_blog
    published: 2025-03-01
    reliability: high
  - id: src6
    title: "Saga Design Pattern — Azure Architecture Center"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/azure/architecture/patterns/saga
    type: official_docs
    published: 2025-02-25
    reliability: authoritative
---

# Saga Pattern for Distributed Transactions Across ERPs

## TL;DR

- **Bottom line**: Use the Saga pattern when a business transaction spans multiple ERPs (e.g., order-to-cash across Salesforce + SAP + a payment gateway) and you cannot use a traditional two-phase commit. Orchestration is the default choice for ERP integrations; choreography only works for simple 2-3 system flows. [src1]
- **Key limit**: Sagas provide eventual consistency, not ACID isolation — concurrent sagas can produce dirty reads, lost updates, and nonrepeatable reads. Design compensating transactions for every forward step. [src2]
- **Watch out for**: Compensating transactions are NOT true rollbacks — reversing an invoice in SAP or a payment in Stripe creates a new offsetting record, not a deletion. Business stakeholders must approve compensation semantics. [src6]
- **Best for**: Long-running, multi-step business processes that cross ERP boundaries — order-to-cash, procure-to-pay, intercompany settlement, multi-system fulfillment. [src1]
- **Authentication**: Each ERP participant authenticates independently — the saga orchestrator needs valid credentials (OAuth 2.0, API keys, certificates) for every system in the chain. [src3]

## System Profile

The Saga pattern is an architecture pattern, not a product. It applies to any distributed transaction that spans multiple ERP systems, middleware platforms, or microservices. This card covers the pattern itself, its two coordination approaches (orchestration and choreography), and practical implementation guidance for cross-ERP business transactions.

Common ERP participants in saga-based integrations include Salesforce, SAP S/4HANA, Oracle ERP Cloud, NetSuite, Microsoft Dynamics 365, and Workday. The pattern is typically implemented using an integration platform (MuleSoft, Boomi, Workato) or a workflow engine (Temporal, AWS Step Functions, Azure Durable Functions, Camunda).

| System | Role | API Surface | Direction |
|---|---|---|---|
| ERP System A (e.g., Salesforce) | CRM — source order/opportunity | REST API | Outbound |
| ERP System B (e.g., SAP S/4HANA) | ERP — financial posting, inventory | OData / BAPI | Inbound |
| Payment Gateway (e.g., Stripe) | Payment processing | REST API | Bidirectional |
| Orchestrator (e.g., Temporal, Step Functions) | Saga coordinator | SDK / State Machine | Orchestrator |

## API Surfaces & Capabilities

The Saga pattern does not define a specific API surface — it defines how multiple API surfaces coordinate. The key capability matrix compares the two coordination approaches: [src1, src2]

| Capability | Orchestration | Choreography |
|---|---|---|
| **Coordination** | Central orchestrator sends commands to participants | Participants publish/subscribe to domain events |
| **Complexity ceiling** | Scales to 10+ participants | Practical limit: 3-5 participants |
| **Visibility** | Single place to inspect saga state | Must aggregate events across all participants |
| **Coupling** | Participants coupled to orchestrator | Participants coupled to event schema |
| **Single point of failure** | Yes — orchestrator (mitigated by HA deployment) | No — distributed |
| **Cyclic dependencies** | Avoided by design | Risk of circular event chains |
| **Error handling** | Orchestrator triggers compensations | Each participant handles failure events independently |
| **Debugging** | Straightforward — follow orchestrator logs | Difficult — requires distributed tracing |
| **Testing** | Test orchestrator + mock participants | Must run all participants to test end-to-end |
| **New participant** | Modify orchestrator workflow | Add new event listener (but verify no side effects) |

## Rate Limits & Quotas

### Per-Saga Limits

Saga throughput is constrained by the slowest participant's API limits. A single business transaction (e.g., order-to-cash) may consume multiple API calls per participant: [src3]

| Saga Step | Typical API Calls | Rate Limit Concern | Mitigation |
|---|---|---|---|
| Create order in CRM | 1-3 (create + related records) | Salesforce: 100K/24h | Batch related records via composite API |
| Reserve inventory in ERP | 1-2 (availability check + reservation) | SAP: depends on ICM config | Use async BAPI for high volume |
| Process payment | 1-2 (authorize + capture) | Stripe: 100 req/s | Built-in rate limiting with retry-after |
| Post financial document | 1-3 (header + line items + post) | Oracle: throttled per tenant | Use batch endpoints where available |
| Compensate on failure | 1 per completed step | Doubles API consumption on failure path | Budget for 2x API calls in failure scenarios |

### Orchestrator Platform Limits

| Platform | Execution Limit | State Size | Timeout | Pricing Model |
|---|---|---|---|---|
| AWS Step Functions (Standard) | Unlimited duration | 256 KB payload | 1 year max | Per state transition ($0.025/1K) |
| AWS Step Functions (Express) | 5 minutes max | 256 KB payload | 5 minutes | Per execution + duration |
| Temporal Cloud | Unlimited duration | 50K events/workflow | No hard timeout | Per action |
| Azure Durable Functions | Unlimited duration | No hard limit (storage-backed) | No hard timeout | Per execution + compute |
| Camunda 8 | Unlimited duration | 1 MB per variable | Configurable | Per execution (cloud) |

[src3, src4]

## Authentication

Each saga participant authenticates independently. The orchestrator must maintain valid credentials for every system in the chain: [src3]

| ERP System | Auth Flow | Token Lifetime | Saga Consideration |
|---|---|---|---|
| Salesforce | OAuth 2.0 JWT Bearer | 2h (configurable) | Token may expire mid-saga for long-running flows — implement token refresh |
| SAP S/4HANA | OAuth 2.0 Client Credentials or Basic Auth | Session-based | CSRF token required for write operations — fetch per saga step |
| Oracle ERP Cloud | OAuth 2.0 or Basic Auth | Session-based | Rate limiting tied to auth identity — use dedicated integration user |
| NetSuite | Token-Based Auth (TBA) | No expiry | Consumer/token key pair — rotate quarterly |
| Stripe | API Key (Secret Key) | No expiry | Use restricted keys with minimal permissions per saga step |

### Authentication Gotchas

- **Token expiry mid-saga**: Long-running sagas (minutes to hours) can outlive access tokens. The orchestrator must refresh tokens before each step, not at saga start. [src3]
- **Credential scope**: Each saga step may need different permission scopes. A single integration user with broad permissions is convenient but violates least-privilege. Use scoped tokens per step where possible. [src4]
- **CSRF tokens**: SAP and some Oracle endpoints require CSRF tokens that expire quickly. Fetch a new CSRF token at each write step, not once at saga initialization.

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- **No ACID isolation**: Concurrent sagas operating on the same data can produce lost updates, dirty reads, and nonrepeatable reads. You must implement application-level countermeasures (semantic locks, commutative updates, reread values). [src2, src6]
- **Compensating transactions are imperfect**: A compensation is a semantic inverse, not a database rollback. Reversing an invoice creates a credit memo; cancelling a payment creates a refund. Business logic must define what "undo" means for each step. [src1]
- **Compensation must be idempotent**: If the compensation message is delivered twice (common in distributed systems), running it again must not cause double-reversal. [src2]
- **Pivot transaction is the point of no return**: Once the pivot transaction succeeds, all subsequent steps must be retryable to completion — they cannot be compensated. Design your saga so the most critical and least reversible step is the pivot. [src6]
- **ERP API rate limits compound**: A 10-step saga consuming 3 API calls per step = 30 calls per business transaction. At 1,000 transactions/day, that is 30,000 API calls. Factor in compensation (failure path doubles consumption). [src3]
- **Eventual consistency window**: Between a forward transaction and its compensation (on failure), other systems see inconsistent data. This window can be seconds to minutes depending on ERP response times.

## Integration Pattern Decision Tree

```
START — Need distributed transaction across multiple ERPs
├── How many systems participate?
│   ├── 2 systems, simple request-reply
│   │   └── SKIP SAGA — use direct API call + retry + dead letter queue
│   ├── 2-3 systems, linear flow
│   │   ├── Team has event infrastructure (Kafka, SNS/SQS)?
│   │   │   ├── YES → Consider CHOREOGRAPHY
│   │   │   └── NO → Use ORCHESTRATION
│   │   └── Need clear visibility into transaction state?
│   │       ├── YES → ORCHESTRATION (single state machine)
│   │       └── NO → CHOREOGRAPHY (if event infra exists)
│   └── 4+ systems or branching/parallel steps
│       └── Always ORCHESTRATION — choreography becomes unmanageable
├── What consistency model is acceptable?
│   ├── Strong ACID required → SAGA IS WRONG PATTERN — use 2PC or redesign
│   ├── Eventual consistency OK → Saga fits
│   └── Best-effort OK → Saga with relaxed compensation
├── How long does the transaction take?
│   ├── < 1 second → Consider synchronous orchestration
│   ├── 1 second - 5 minutes → Standard saga (Step Functions Express or Temporal)
│   └── > 5 minutes (human approvals, batch ERP jobs)
│       └── Long-running saga (Step Functions Standard, Temporal, Camunda)
├── Which orchestration platform?
│   ├── AWS ecosystem → Step Functions
│   ├── Azure ecosystem → Durable Functions
│   ├── Multi-cloud / on-premise → Temporal or Camunda
│   └── iPaaS (MuleSoft, Boomi, Workato) → Built-in workflow engine
└── Error tolerance?
    ├── Zero-loss required → Idempotent steps + dead letter queue + manual review
    └── Best-effort acceptable → Retry with exponential backoff + alerting
```

## Quick Reference

### Saga Transaction Types [src2, src6]

| Transaction Type | Description | Can Be Undone? | Example in ERP Context |
|---|---|---|---|
| **Compensable** | Forward step that can be semantically reversed | Yes | Create sales order in SAP (→ cancel order) |
| **Pivot** | Point of no return — once committed, saga must complete | After this: No | Submit payment to gateway (→ cannot un-charge, only refund) |
| **Retryable** | Steps after pivot that must eventually succeed | N/A (must succeed) | Send shipment notification email, update CRM status |

### Orchestration vs Choreography Decision Matrix [src1, src2, src5]

| Criterion | Choose Orchestration | Choose Choreography |
|---|---|---|
| **Participant count** | 4+ systems | 2-3 systems |
| **Workflow complexity** | Branching, parallel, conditional | Linear, sequential |
| **Team structure** | Central integration team | Independent service teams |
| **Debugging needs** | Must trace full transaction | Per-service debugging sufficient |
| **Coupling tolerance** | Participants coupled to orchestrator | Participants coupled to events |
| **Failure recovery** | Centralized compensation logic | Distributed compensation logic |
| **ERP integrations** | Almost always (ERP APIs are complex) | Rarely (ERP complexity demands central control) |

### Compensating Transaction Reference for Common ERP Operations

| Forward Transaction | Compensating Transaction | System | Gotcha |
|---|---|---|---|
| Create Sales Order | Cancel Sales Order + release inventory | SAP S/4HANA | Cancellation creates a new document — does not delete original |
| Reserve Inventory | Release Reservation | Any ERP | Check if reservation has already been consumed by fulfillment |
| Authorize Payment | Void Authorization | Stripe/Payment Gateway | Void only works before capture — after capture, must refund |
| Post Accounting Entry | Post Reversal Entry | Oracle ERP / SAP | Reversal is a new journal entry — audit trail preserved |
| Create Invoice | Issue Credit Memo | Any ERP | Credit memo must reference original invoice number |
| Update Customer Record | Restore Previous Values | Salesforce | Requires storing pre-update snapshot — no auto-versioning |
| Allocate Budget | Deallocate Budget | Workday / SAP | Check if budget was already consumed by downstream processes |

## Step-by-Step Integration Guide

### 1. Design the saga: identify steps, pivot, and compensations

Map every step in the business transaction. For each step, define the forward transaction, the compensating transaction, and whether it is compensable, pivot, or retryable. [src1, src6]

```
Example: Order-to-Cash Saga

Step 1 (Compensable): Create Sales Order in SAP
  → Forward: POST /sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder
  → Compensate: PATCH /.../{SalesOrder} with OverallSDProcessStatus = 'C' (cancel)

Step 2 (Compensable): Reserve Inventory in WMS
  → Forward: POST /api/v1/reservations
  → Compensate: DELETE /api/v1/reservations/{id}

Step 3 (PIVOT): Capture Payment via Stripe
  → Forward: POST /v1/payment_intents/{id}/capture
  → No compensation (refund is a separate business decision, not auto-triggered)

Step 4 (Retryable): Post Revenue Recognition in Oracle ERP
  → Forward: POST /fscmRestApi/resources/latest/receivablesInvoices
  → Must succeed — retry with exponential backoff

Step 5 (Retryable): Update Opportunity Status in Salesforce
  → Forward: PATCH /services/data/v62.0/sobjects/Opportunity/{id}
  → Must succeed — retry with backoff
```

**Verify**: Review with business stakeholders — they must agree on what "undo" means for each step.

### 2. Implement the orchestrator

Choose a workflow engine and implement the saga state machine. This example uses Temporal (TypeScript SDK): [src4]

```typescript
// saga-orchestrator.ts — Temporal Workflow
import { proxyActivities, ApplicationFailure } from '@temporalio/workflow';
import type * as activities from './activities';

const {
  createSalesOrder, cancelSalesOrder,
  reserveInventory, releaseInventory,
  capturePayment,
  postRevenueRecognition,
  updateCrmStatus
} = proxyActivities<typeof activities>({
  startToCloseTimeout: '30s',
  retry: { maximumAttempts: 3, backoffCoefficient: 2 }
});

export async function orderToCashSaga(order: OrderInput): Promise<SagaResult> {
  const compensations: Array<() => Promise<void>> = [];

  try {
    // Step 1: Compensable — Create Sales Order
    const salesOrder = await createSalesOrder(order);
    compensations.push(() => cancelSalesOrder(salesOrder.id));

    // Step 2: Compensable — Reserve Inventory
    const reservation = await reserveInventory(order.items);
    compensations.push(() => releaseInventory(reservation.id));

    // Step 3: PIVOT — Capture Payment (point of no return)
    await capturePayment(order.paymentIntentId);
    // After this point, no more compensations — only retryable steps

    // Step 4: Retryable — Post Revenue Recognition
    await postRevenueRecognition(salesOrder.id, order.amount);

    // Step 5: Retryable — Update CRM
    await updateCrmStatus(order.opportunityId, 'Closed Won');

    return { status: 'completed', salesOrderId: salesOrder.id };
  } catch (error) {
    // Execute compensations in reverse order
    for (const compensate of compensations.reverse()) {
      try {
        await compensate();
      } catch (compError) {
        // Log compensation failure — requires manual intervention
        throw ApplicationFailure.nonRetryable(
          `Compensation failed: ${compError}. Manual intervention required.`
        );
      }
    }
    return { status: 'compensated', error: String(error) };
  }
}
```

**Verify**: Deploy workflow → trigger with test order → confirm all steps execute or compensate correctly.

### 3. Implement idempotent participants

Each saga participant must be idempotent — processing the same request twice must produce the same result without side effects. Use idempotency keys: [src2, src3]

```typescript
// activities.ts — Idempotent saga participant
import { activityInfo } from '@temporalio/activity';

export async function createSalesOrder(order: OrderInput): Promise<SalesOrder> {
  const idempotencyKey = `saga-${activityInfo().workflowExecution.workflowId}-create-so`;

  // Check if this step already completed (idempotency)
  const existing = await sapClient.get(`/A_SalesOrder?$filter=YY1_IdempotencyKey eq '${idempotencyKey}'`);
  if (existing.d.results.length > 0) {
    return existing.d.results[0]; // Already created — return existing
  }

  // Create new sales order with idempotency key
  const result = await sapClient.post('/A_SalesOrder', {
    SalesOrderType: 'OR',
    SoldToParty: order.customerId,
    YY1_IdempotencyKey: idempotencyKey,
    to_Item: order.items.map(item => ({
      Material: item.sku,
      RequestedQuantity: item.quantity
    }))
  });

  return result.d;
}
```

**Verify**: Call `createSalesOrder` twice with the same workflow ID → confirm only one sales order exists in SAP.

### 4. Implement compensation with pre-update snapshots

For steps that update existing records (not create new ones), store the previous state so compensation can restore it: [src1]

```typescript
export async function updateCrmStatus(
  opportunityId: string,
  newStatus: string
): Promise<{ previousStatus: string }> {
  // Read current state BEFORE updating (for compensation)
  const current = await salesforceClient.get(
    `/services/data/v62.0/sobjects/Opportunity/${opportunityId}?fields=StageName`
  );
  const previousStatus = current.StageName;

  // Update to new status
  await salesforceClient.patch(
    `/services/data/v62.0/sobjects/Opportunity/${opportunityId}`,
    { StageName: newStatus }
  );

  return { previousStatus };
}

export async function revertCrmStatus(
  opportunityId: string,
  previousStatus: string
): Promise<void> {
  await salesforceClient.patch(
    `/services/data/v62.0/sobjects/Opportunity/${opportunityId}`,
    { StageName: previousStatus }
  );
}
```

**Verify**: Update opportunity → trigger compensation → confirm StageName reverted to original value.

## Code Examples

### Python: Saga Orchestrator with Temporal

```python
# Input:  Order details (customer_id, items, payment_intent_id)
# Output: Saga result (completed or compensated)

from temporalio import workflow, activity
from dataclasses import dataclass
from typing import List, Callable, Awaitable

@dataclass
class OrderInput:
    customer_id: str
    items: list
    payment_intent_id: str
    opportunity_id: str

@workflow.defn
class OrderToCashSaga:
    @workflow.run
    async def run(self, order: OrderInput) -> dict:
        compensations: List[Callable[[], Awaitable[None]]] = []

        try:
            # Step 1: Compensable — Create Sales Order in SAP
            sales_order = await workflow.execute_activity(
                create_sales_order, order,
                start_to_close_timeout=timedelta(seconds=30),
            )
            compensations.append(
                lambda so_id=sales_order["id"]: workflow.execute_activity(
                    cancel_sales_order, so_id,
                    start_to_close_timeout=timedelta(seconds=30),
                )
            )

            # Step 2: Compensable — Reserve Inventory
            reservation = await workflow.execute_activity(
                reserve_inventory, order.items,
                start_to_close_timeout=timedelta(seconds=30),
            )
            compensations.append(
                lambda res_id=reservation["id"]: workflow.execute_activity(
                    release_inventory, res_id,
                    start_to_close_timeout=timedelta(seconds=30),
                )
            )

            # Step 3: PIVOT — Capture Payment
            await workflow.execute_activity(
                capture_payment, order.payment_intent_id,
                start_to_close_timeout=timedelta(seconds=30),
            )

            # Step 4-5: Retryable — post-pivot steps
            await workflow.execute_activity(
                post_revenue, sales_order["id"],
                start_to_close_timeout=timedelta(seconds=60),
                retry_policy=RetryPolicy(maximum_attempts=10),
            )
            await workflow.execute_activity(
                update_crm, order.opportunity_id,
                start_to_close_timeout=timedelta(seconds=30),
                retry_policy=RetryPolicy(maximum_attempts=10),
            )

            return {"status": "completed", "sales_order_id": sales_order["id"]}

        except Exception as e:
            # Compensate in reverse order
            for compensate in reversed(compensations):
                await compensate()
            return {"status": "compensated", "error": str(e)}
```

### JavaScript/Node.js: AWS Step Functions State Machine Definition

```javascript
// Input:  Order event from API Gateway
// Output: Step Functions execution ARN + saga result

// step-function-definition.json — saga state machine
const sagaDefinition = {
  Comment: "Order-to-Cash Saga with Compensations",
  StartAt: "CreateSalesOrder",
  States: {
    CreateSalesOrder: {
      Type: "Task",
      Resource: "arn:aws:lambda:us-east-1:123456:function:createSalesOrder",
      ResultPath: "$.salesOrder",
      Next: "ReserveInventory",
      Catch: [{ ErrorEquals: ["States.ALL"], Next: "SagaFailed" }]
    },
    ReserveInventory: {
      Type: "Task",
      Resource: "arn:aws:lambda:us-east-1:123456:function:reserveInventory",
      ResultPath: "$.reservation",
      Next: "CapturePayment",
      Catch: [{ ErrorEquals: ["States.ALL"], Next: "CancelSalesOrder" }]
    },
    CapturePayment: {  // PIVOT transaction
      Type: "Task",
      Resource: "arn:aws:lambda:us-east-1:123456:function:capturePayment",
      ResultPath: "$.payment",
      Next: "PostRevenue",
      Catch: [{ ErrorEquals: ["States.ALL"], Next: "ReleaseInventory" }]
    },
    PostRevenue: {  // Retryable
      Type: "Task",
      Resource: "arn:aws:lambda:us-east-1:123456:function:postRevenue",
      Retry: [{ ErrorEquals: ["States.ALL"], MaxAttempts: 5, BackoffRate: 2 }],
      Next: "UpdateCRM"
    },
    UpdateCRM: {  // Retryable
      Type: "Task",
      Resource: "arn:aws:lambda:us-east-1:123456:function:updateCRM",
      Retry: [{ ErrorEquals: ["States.ALL"], MaxAttempts: 5, BackoffRate: 2 }],
      End: true
    },
    // Compensation chain (reverse order)
    ReleaseInventory: {
      Type: "Task",
      Resource: "arn:aws:lambda:us-east-1:123456:function:releaseInventory",
      Next: "CancelSalesOrder"
    },
    CancelSalesOrder: {
      Type: "Task",
      Resource: "arn:aws:lambda:us-east-1:123456:function:cancelSalesOrder",
      Next: "SagaFailed"
    },
    SagaFailed: {
      Type: "Fail",
      Error: "SagaCompensated",
      Cause: "One or more saga steps failed; compensations executed."
    }
  }
};
```

### cURL: Testing a Saga Step (SAP Sales Order Creation)

```bash
# Input:  SAP OAuth token, order payload
# Output: Sales order ID or error

# Step 1: Get CSRF token (required for SAP write operations)
curl -s -X GET \
  "https://my-sap.s4hana.cloud/sap/opu/odata/sap/API_SALES_ORDER_SRV/" \
  -H "Authorization: Bearer $SAP_TOKEN" \
  -H "x-csrf-token: fetch" \
  -D - -o /dev/null 2>/dev/null | grep x-csrf-token

# Step 2: Create Sales Order
curl -X POST \
  "https://my-sap.s4hana.cloud/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder" \
  -H "Authorization: Bearer $SAP_TOKEN" \
  -H "x-csrf-token: $CSRF_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "SalesOrderType": "OR",
    "SoldToParty": "CUST001",
    "to_Item": [{
      "Material": "MAT001",
      "RequestedQuantity": "10",
      "RequestedQuantityUnit": "EA"
    }]
  }'

# Expected response: 201 Created with SalesOrder number in response body
# Compensation: PATCH same endpoint with OverallSDProcessStatus = "C"
```

## Data Mapping

### Saga State Mapping Across Systems

| Saga State | Salesforce (CRM) | SAP S/4HANA (ERP) | Stripe (Payment) | Orchestrator |
|---|---|---|---|---|
| **Initiated** | Opportunity: Negotiation | No record yet | PaymentIntent: created | Saga: RUNNING |
| **Order Created** | Opportunity: Negotiation | Sales Order: Open | PaymentIntent: created | Step 1: COMPLETE |
| **Inventory Reserved** | Opportunity: Negotiation | Sales Order: Open, Reservation created | PaymentIntent: created | Step 2: COMPLETE |
| **Payment Captured** | Opportunity: Closed Won | Sales Order: Open | PaymentIntent: succeeded | PIVOT PASSED |
| **Revenue Posted** | Opportunity: Closed Won | Billing Document created | PaymentIntent: succeeded | Step 4: COMPLETE |
| **Completed** | Opportunity: Closed Won | Sales Order: Completed | PaymentIntent: succeeded | Saga: COMPLETED |
| **Compensating** | Opportunity: reverted | Sales Order: Cancelled | PaymentIntent: canceled/refunded | Saga: COMPENSATING |
| **Failed** | Opportunity: Closed Lost | Sales Order: Cancelled | PaymentIntent: canceled | Saga: FAILED |

### Data Type Gotchas

- **Currency precision**: Stripe amounts are in smallest currency unit (cents). SAP stores in full unit with decimal. Salesforce stores as decimal. Always convert before passing between systems. [src3]
- **Date/time zones**: Salesforce stores UTC. SAP depends on user profile (can be local timezone). Oracle ERP uses server timezone. Normalize to UTC at the orchestrator level. [src2]
- **ID formats**: SAP sales orders are numeric (10 digits, zero-padded). Salesforce IDs are 18-character alphanumeric. Stripe IDs are prefixed strings (pi_xxx). The orchestrator must maintain a cross-reference map. [src3]
- **Status enums are system-specific**: "Cancelled" in Salesforce is not the same field value as "Cancelled" in SAP. Map each system's status vocabulary in the orchestrator. [src6]

## Error Handling & Failure Points

### Common Error Scenarios

| Scenario | Impact | Detection | Resolution |
|---|---|---|---|
| Orchestrator crashes mid-saga | Saga hangs in partial state | Heartbeat timeout | Use durable workflow engine (Temporal, Step Functions) — auto-resumes from last checkpoint [src4] |
| ERP API timeout (no response) | Unknown state — did the step succeed? | Timeout threshold | Read-before-write: query ERP for idempotency key before retrying [src2] |
| Compensation fails | System stuck in inconsistent state | Compensation error handler | Dead letter queue + manual intervention alert [src3] |
| Rate limit exceeded (429) | Step cannot execute | HTTP 429 response | Exponential backoff: wait 2^n seconds, max 5 retries [src3] |
| Concurrent saga conflict | Lost update or dirty read | Optimistic locking failure (409/412) | Semantic lock or reread-and-retry pattern [src6] |
| Network partition between orchestrator and participant | Participant may have executed but orchestrator does not know | Timeout + no response | Idempotent retry — participant checks if step already completed [src2] |

### Failure Points in Production

- **Compensation timeout cascade**: If one compensation step takes too long, subsequent compensations may time out. Fix: `Set independent timeouts for each compensation step; do not chain timeouts.` [src3]
- **Saga state persistence loss**: In-memory orchestrators lose state on crash. Fix: `Use durable state persistence (Temporal event sourcing, Step Functions built-in state, database-backed state).` [src4]
- **ERP maintenance windows**: Scheduled ERP downtime causes sagas to fail mid-flight. Fix: `Implement circuit breaker pattern; queue pending sagas and resume after maintenance window.` [src3]
- **Idempotency key collision**: Two different sagas accidentally share an idempotency key. Fix: `Include workflow execution ID + step name in the idempotency key — never use user-supplied values alone.` [src2]
- **Compensation ordering violation**: Compensations executed out of reverse order can cause data integrity issues (e.g., releasing inventory before cancelling the order). Fix: `Always compensate in strict reverse order; use the orchestrator's compensation stack.` [src6]
- **Stale CSRF/auth tokens during long sagas**: Token fetched at saga start expires before a later step executes. Fix: `Fetch fresh auth tokens immediately before each ERP API call, not at saga initialization.` [src3]

## Anti-Patterns

### Wrong: Using 2PC (Two-Phase Commit) across ERP APIs

```
// BAD — 2PC requires all participants to hold locks simultaneously
// ERP APIs do not support distributed lock coordination
BEGIN DISTRIBUTED TRANSACTION
  → Lock row in Salesforce  (not possible via REST API)
  → Lock row in SAP         (not possible via OData API)
  → Lock row in Stripe      (not possible)
COMMIT ALL
// Result: impossible to implement, or fragile custom locking that breaks at scale
```

### Correct: Use Saga with compensating transactions

```
// GOOD — each step commits independently; compensations handle failure
Step 1: Create order in SAP (committed immediately)
Step 2: Reserve inventory (committed immediately)
Step 3: Capture payment (PIVOT — committed immediately)
  → If Step 2 fails: compensate Step 1 (cancel order)
  → If Step 3 fails: compensate Step 2 (release inventory) → compensate Step 1
```

[src1, src2]

### Wrong: Fire-and-forget without compensation design

```javascript
// BAD — no compensation logic, no idempotency, no error handling
async function processOrder(order) {
  await createSapOrder(order);       // What if this succeeds but next fails?
  await reserveInventory(order);     // What if this succeeds but next fails?
  await capturePayment(order);       // What if this fails? SAP order and inventory are orphaned
  await postRevenue(order);
  // No compensation chain. On failure: manual cleanup required.
}
```

### Correct: Every forward step has a compensation partner

```javascript
// GOOD — compensation stack ensures cleanup on any failure
async function processOrderSaga(order) {
  const compensations = [];
  try {
    const so = await createSapOrder(order);
    compensations.push(() => cancelSapOrder(so.id));

    const res = await reserveInventory(order);
    compensations.push(() => releaseInventory(res.id));

    await capturePayment(order);  // PIVOT — no compensation after this

    await postRevenue(so.id);     // Retryable
  } catch (error) {
    for (const comp of compensations.reverse()) {
      await comp();  // Undo in reverse order
    }
    throw error;
  }
}
```

[src1, src4]

### Wrong: Choreography with 6+ ERP participants

```
// BAD — choreography with many participants creates invisible dependency web
OrderService → emits OrderCreated
InventoryService → listens OrderCreated, emits InventoryReserved
PaymentService → listens InventoryReserved, emits PaymentCaptured
ShippingService → listens PaymentCaptured, emits ShipmentCreated
AccountingService → listens PaymentCaptured, emits RevenuePosted
NotificationService → listens ShipmentCreated, emits CustomerNotified
AnalyticsService → listens everything
// Debug question: "Why did this order fail?" → trace 7 event streams across 7 services
```

### Correct: Orchestration for complex multi-ERP flows

```
// GOOD — single orchestrator owns the flow, all state visible in one place
Orchestrator → createOrder(SAP) → reserveInventory(WMS) → capturePayment(Stripe)
            → postRevenue(Oracle) → updateCRM(Salesforce) → notifyCustomer(Email)
// Debug question: "Why did this order fail?" → check orchestrator execution log
```

[src1, src2, src5]

## Common Pitfalls

- **Treating compensation as rollback**: Compensating transactions create new records (credit memos, refunds, cancellation documents) — they do not delete the original. Business stakeholders must approve compensation semantics before implementation. Fix: `Document compensation behavior for each step; get business sign-off.` [src1]
- **Missing idempotency in participants**: Network retries can deliver the same saga command twice. Without idempotency, you get duplicate orders, double charges, or double refunds. Fix: `Use idempotency keys derived from workflow execution ID + step name in every participant.` [src2]
- **Not accounting for ERP API rate limits in saga throughput**: A 10-step saga with compensation paths can consume 20-30 API calls per transaction. At scale, this exhausts ERP API quotas. Fix: `Calculate worst-case API consumption per saga; set saga concurrency limits below ERP rate limits.` [src3]
- **Ignoring the eventual consistency window**: Between a forward step and its potential compensation, other systems see committed but potentially inconsistent data. Fix: `Use semantic locks (status flags like "pending_confirmation") to signal that data may change.` [src6]
- **Hardcoding saga timeout**: Different ERP APIs have wildly different response times (SAP BAPI: 200ms; Oracle FBDI batch: 10 minutes). A single timeout for all steps causes premature failures or unnecessary waits. Fix: `Set per-step timeouts based on the specific ERP API's SLA.` [src3]
- **Forgetting to test the compensation path**: Teams test the happy path but not the failure + compensation path. In production, compensation failures are the most damaging. Fix: `Include chaos testing — inject failures at each step and verify full compensation chain.` [src4]

## Diagnostic Commands

```bash
# Check Temporal workflow status (saga execution)
tctl workflow describe --workflow_id "order-saga-12345"

# List running sagas in Temporal
tctl workflow list --query "WorkflowType='OrderToCashSaga' AND ExecutionStatus='Running'"

# Check AWS Step Functions execution
aws stepfunctions describe-execution \
  --execution-arn "arn:aws:states:us-east-1:123456:execution:OrderSaga:exec-001"

# List failed saga executions in Step Functions (last 24h)
aws stepfunctions list-executions \
  --state-machine-arn "arn:aws:states:us-east-1:123456:stateMachine:OrderSaga" \
  --status-filter "FAILED" \
  --max-results 50

# Check SAP API call quota remaining
curl -s -X GET "https://my-sap.s4hana.cloud/sap/opu/odata/sap/API_SALES_ORDER_SRV/" \
  -H "Authorization: Bearer $SAP_TOKEN" -D - -o /dev/null 2>/dev/null | grep -i "rate\|limit\|quota"

# Check Salesforce API usage
curl -s "https://yourorg.my.salesforce.com/services/data/v62.0/limits" \
  -H "Authorization: Bearer $SF_TOKEN" | jq '.DailyApiRequests'

# Monitor Stripe payment intent status (verify saga pivot)
curl -s "https://api.stripe.com/v1/payment_intents/pi_xxx" \
  -u "$STRIPE_SECRET_KEY:" | jq '{status, amount, currency}'
```

## Version History & Compatibility

| Pattern / Platform | Version | Date | Status | Key Changes |
|---|---|---|---|---|
| Saga pattern (original paper) | Hector Garcia-Molina & Kenneth Salem | 1987 | Foundational | Original concept for long-lived transactions |
| Microservices adaptation (Chris Richardson) | microservices.io | 2018 | Current reference | Adapted for microservices with orchestration/choreography |
| AWS Step Functions | Standard & Express | 2024 | GA | Built-in Map state for parallel saga steps |
| Temporal | 1.x | 2024-2025 | GA | Durable execution, auto-retry, versioning |
| Azure Durable Functions | v4 | 2024 | GA | Sub-orchestrations for nested sagas |
| Camunda 8 | 8.5+ | 2024 | GA | Improved BPMN compensation events |

[src1, src2, src3, src4]

### Pattern Evolution

The Saga pattern was first described in a 1987 paper by Garcia-Molina and Salem for long-lived database transactions. It was adapted for microservices by Chris Richardson circa 2018 and has since become the dominant pattern for distributed transactions in cloud-native architectures. The core pattern is stable; what evolves are the orchestration platforms and their capabilities. [src1]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Business transaction spans 3+ ERP systems | Transaction fits within a single ERP/database | Native ERP transaction (SAP LUW, Oracle DB transaction) |
| Eventual consistency is acceptable to the business | Strong ACID consistency required across all systems | Two-phase commit (if participants support it) — rare for ERP APIs |
| Steps take seconds to minutes (API calls to ERPs) | Steps are sub-millisecond (in-memory operations) | Local transaction with outbox pattern |
| Human approval steps may be involved | All steps are instantaneous and reversible | Simple retry with dead letter queue |
| Multiple teams own different participants | Single team owns all systems | Direct orchestration without saga formalism |
| Failure at any step is a realistic scenario | All downstream systems have 99.999% uptime | Direct sequential calls with retry |

## Cross-System Comparison

| Capability | Saga (Orchestration) | Saga (Choreography) | Two-Phase Commit (2PC) | Outbox + Eventual Consistency |
|---|---|---|---|---|
| **Consistency model** | Eventual | Eventual | Strong (ACID) | Eventual |
| **Participant coupling** | To orchestrator | To event schema | To coordinator | To message broker |
| **Scalability** | High (async steps) | High (event-driven) | Low (locks held during prepare) | High |
| **Complexity** | Medium-High | High (at scale) | Low (but limited applicability) | Low-Medium |
| **ERP API compatible** | Yes | Yes | No (ERPs don't expose prepare/commit) | Yes (single-system) |
| **Multi-ERP transactions** | Yes — primary use case | Possible but hard to manage | Not viable | No — single system only |
| **Failure recovery** | Automatic compensation | Distributed compensation | Automatic rollback | Retry with idempotency |
| **Debugging** | Good (single orchestrator log) | Poor (distributed event traces) | Good (coordinator log) | Medium |
| **Latency overhead** | Medium (sequential steps + state persistence) | Low (parallel event processing) | High (lock duration) | Low |
| **Tooling maturity** | Temporal, Step Functions, Durable Functions | Kafka, EventBridge, SNS/SQS | Database-native only | Debezium, Outbox libraries |

[src1, src2, src3, src6]

## Important Caveats

- **Compensation is a business decision, not a technical one**: What "undo" means for each step must be defined by business stakeholders. A technical team cannot decide unilaterally whether a failed order should auto-refund or require manual review. [src1]
- **Eventual consistency window is visible to users**: Between steps, the system is in an intermediate state. A customer may see "order confirmed" in the CRM before payment is captured. Use semantic status flags ("pending confirmation") to communicate the saga's progress. [src6]
- **Saga complexity compounds with participant count**: Each new participant adds forward + compensation steps, multiplies failure scenarios, and increases testing surface area. Resist the urge to put everything in one saga. Split large business processes into smaller, independent sagas connected by domain events. [src2]
- **Platform lock-in risk**: Choosing AWS Step Functions, Azure Durable Functions, or Temporal creates vendor/platform dependency for your most critical business transactions. The saga pattern itself is portable; the implementation is not. [src3, src4]
- **ERP API changes break sagas**: When an ERP vendor changes API behavior (field names, status codes, rate limits), saga steps that worked yesterday may fail. Pin API versions and monitor for deprecation notices. [src3]
- **This card describes the pattern at an architecture level**: Specific implementation details vary significantly by orchestration platform, ERP system, and programming language. Always consult the relevant ERP API documentation and platform guides for production implementation.

## Related Units

- [Event-driven ERP integration patterns](/business/erp-integration/event-driven-erp-integration/2026)
- [Order-to-cash cross-ERP integration](/business/erp-integration/order-to-cash-erp-integration/2026)
- [Procure-to-pay cross-ERP integration](/business/erp-integration/procure-to-pay-erp-integration/2026)
