---
# === IDENTITY ===
id: business/erp-integration/compensating-transactions/2026
canonical_question: "How do you implement compensating transactions when saga rollback is not possible in ERPs?"
aliases:
  - "Compensating transactions for ERP integration saga failures"
  - "How to undo committed ERP transactions across distributed systems"
  - "ERP compensation patterns when rollback is impossible"
  - "Designing compensating actions for cross-ERP saga workflows"
entity_type: erp_integration
domain: business > erp-integration > compensating-transactions
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Cross-ERP (Architecture Pattern)"
    vendor: "N/A — architecture pattern"
    version: "N/A"
    edition: "All"
    deployment: "cloud"
    api_surface: "REST, Event Bus, Message Queue"
  - name: "Salesforce"
    vendor: "Salesforce"
    version: "API v62.0"
    edition: "Enterprise, Unlimited"
    deployment: "cloud"
    api_surface: "REST"
  - name: "SAP S/4HANA"
    vendor: "SAP"
    version: "2408"
    edition: "Cloud & On-Premise"
    deployment: "hybrid"
    api_surface: "OData, BAPI"
  - name: "Oracle NetSuite"
    vendor: "Oracle"
    version: "2024.2"
    edition: "All"
    deployment: "cloud"
    api_surface: "REST, SuiteTalk SOAP"
  - name: "Microsoft Dynamics 365 Finance"
    vendor: "Microsoft"
    version: "10.0.40"
    edition: "All"
    deployment: "cloud"
    api_surface: "OData v4"

# === VERIFICATION ===
last_verified: 2026-03-07
confidence: 0.85
version: 1.0
first_published: 2026-03-07

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: null
  next_review: 2026-09-03
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "ERPs do NOT support distributed transactions (2PC) across system boundaries — each ERP commits independently"
  - "Compensating transactions are semantic inverses, NOT true rollbacks — a credit reversal is not the same as 'never charged'"
  - "Some ERP operations are inherently non-compensable (e.g., email notifications, physical shipments, printed checks)"
  - "SAP reversal documents (FB08) require the original posting period to be open — closed periods block compensation"
  - "NetSuite void vs. reverse behavior depends on the 'Void Transactions Using Reversing Journals' preference — cannot use both simultaneously"
  - "Salesforce hard-deleted records cannot be undeleted — only soft-deleted records in the Recycle Bin (15-day retention) can be recovered"
  - "Compensation must be idempotent — network failures can cause compensating actions to execute multiple times"
  - "Compensation of compensation (nested failures) requires manual intervention or a dead letter queue — there is no infinite recursion solution"

# === 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 wants the Saga pattern overview (orchestration vs choreography)"
    use_instead: "business/erp-integration/saga-pattern-erp-transactions/2026"
  - condition: "User needs error handling and dead letter queue patterns"
    use_instead: "business/erp-integration/error-handling-dead-letter-queues/2026"
  - condition: "User needs idempotency patterns for ERP API calls"
    use_instead: "business/erp-integration/idempotency-erp-integrations/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: compensation_scope
    question: "What is the scope of your compensation need?"
    type: choice
    options:
      - "single ERP operation reversal"
      - "multi-step saga across 2-3 ERPs"
      - "long-running business process (days/weeks)"
      - "batch/bulk operation rollback"
  - key: erp_systems
    question: "Which ERP systems are involved?"
    type: choice
    options:
      - "Salesforce"
      - "SAP S/4HANA"
      - "Oracle NetSuite"
      - "Microsoft Dynamics 365"
      - "Multiple / Cross-system"
  - key: failure_tolerance
    question: "What happens if compensation itself fails?"
    type: choice
    options:
      - "must succeed — financial/regulatory requirement"
      - "best-effort with manual fallback"
      - "can retry indefinitely until resolved"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/compensating-transactions/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-07)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "business/erp-integration/saga-pattern-erp-transactions/2026"
      label: "Saga Pattern for ERP Distributed Transactions"
    - id: "business/erp-integration/idempotency-erp-integrations/2026"
      label: "Idempotency Patterns for ERP API Calls"
  related_to:
    - id: "business/erp-integration/error-handling-dead-letter-queues/2026"
      label: "Error Handling and Dead Letter Queues"
    - id: "business/erp-integration/change-data-capture-erp/2026"
      label: "Change Data Capture for ERP Integration"
  solves:
    - id: "business/erp-integration/order-to-cash-integration/2026"
      label: "Order-to-Cash Integration (uses compensation for failed fulfillment)"
    - id: "business/erp-integration/procure-to-pay-integration/2026"
      label: "Procure-to-Pay Integration (uses compensation for rejected invoices)"
  alternative_to:
    - id: "business/erp-integration/batch-vs-realtime-integration/2026"
      label: "Batch vs Real-time (batch can use simpler rollback strategies)"
  often_confused_with:
    - id: "business/erp-integration/saga-pattern-erp-transactions/2026"
      label: "Saga Pattern — compensation is one mechanism within sagas, not the same thing"

# === SOURCES ===
sources:
  - id: src1
    title: "Compensating Transaction Pattern"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction
    type: technical_blog
    published: 2025-12-09
    reliability: high
  - id: src2
    title: "Saga Compensating Transactions"
    author: Temporal Technologies
    url: https://temporal.io/blog/compensating-actions-part-of-a-complete-breakfast-with-sagas
    type: technical_blog
    published: 2024-06-15
    reliability: high
  - id: src3
    title: "Saga Design Pattern"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/azure/architecture/patterns/saga
    type: technical_blog
    published: 2025-12-09
    reliability: high
  - id: src4
    title: "Document Reversal FB08 in SAP: Step by Step Guide"
    author: Guru99
    url: https://www.guru99.com/how-to-perform-document-reversal.html
    type: community_resource
    published: 2025-01-15
    reliability: moderate_high
  - id: src5
    title: "Voiding, Deleting, or Closing Transactions — Oracle NetSuite Help"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_N563543.html
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src6
    title: "Cancel vs. Reverse in Microsoft Dynamics 365 Business Central"
    author: Microsoft Dynamics Community
    url: https://community.dynamics.com/blogs/post/?postid=52c84cd6-8317-ef11-989a-000d3ae2aa43
    type: community_resource
    published: 2025-09-10
    reliability: moderate_high
  - id: src7
    title: "Compensation Transaction Patterns — Orkes"
    author: Orkes
    url: https://orkes.io/blog/compensation-transaction-patterns/
    type: technical_blog
    published: 2025-03-20
    reliability: moderate_high
  - id: src8
    title: "undelete() — Salesforce SOAP API Developer Guide"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_undelete.htm
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
---

# Compensating Transactions for ERP Integration

## TL;DR

- **Bottom line**: ERPs cannot participate in distributed transactions (2PC), so every multi-system workflow must use compensating transactions — new operations that semantically undo prior committed work — instead of database rollbacks.
- **Key limit**: Some ERP operations are non-compensable (emails sent, checks printed, goods shipped). Design your saga steps so non-compensable actions execute last.
- **Watch out for**: Compensation failures — if the compensating transaction itself fails, you need a dead letter queue and manual reconciliation process, not recursive compensation.
- **Best for**: Multi-ERP workflows (order-to-cash, procure-to-pay, hire-to-retire) where a failure at step N requires undoing steps 1 through N-1 across system boundaries.
- **Pattern choice**: Use saga orchestration (not choreography) when compensation logic is complex; use Temporal or AWS Step Functions as the orchestrator for durable execution guarantees.

## System Profile

This is a cross-system architecture pattern card covering compensating transaction design across major ERP platforms. It applies to any integration where two or more ERPs participate in a multi-step business process and true distributed transaction support (2PC/XA) is unavailable — which is virtually every cloud ERP integration scenario. The card covers per-ERP compensation capabilities for Salesforce, SAP S/4HANA, Oracle NetSuite, and Microsoft Dynamics 365, plus orchestration tooling (Temporal, AWS Step Functions).

| System | Role | Compensation Mechanism | Direction |
|---|---|---|---|
| Salesforce | CRM — opportunity/order source | Undelete, status reversal, credit memos | Outbound |
| SAP S/4HANA | ERP — financial/logistics master | Reversal documents (FB08), storno, cancellation | Inbound |
| Oracle NetSuite | ERP — mid-market financial | Void (transaction.void), reversing journals | Inbound |
| Microsoft Dynamics 365 | ERP — finance and operations | Cancel, reverse, corrective posting | Inbound |
| Orchestrator (Temporal/Step Functions) | Saga coordinator | Compensation registry, retry, DLQ | N/A |

## Per-ERP Compensation Capabilities

### What Can and Cannot Be Reversed

Understanding each ERP's reversal capabilities is the foundation of compensation design. Every ERP handles "undo" differently, and some operations simply cannot be reversed via API. [src4, src5, src6, src8]

| ERP | Operation | Compensation Method | API Support | Limitations |
|---|---|---|---|---|
| **Salesforce** | Record create | Delete (soft) → Recycle Bin | REST DELETE /sobjects/{Object}/{Id} | Hard-deleted records cannot be recovered |
| **Salesforce** | Record delete (soft) | Undelete | SOAP undelete() or REST PATCH | 15-day Recycle Bin retention limit |
| **Salesforce** | Opportunity close | Reopen (status change) | REST PATCH StageName | Dependent workflows may have already fired |
| **Salesforce** | Email send | Non-compensable | N/A | Cannot unsend — send a follow-up correction |
| **SAP** | FI document posting | Reversal document (FB08/BAPI_ACC_DOCUMENT_REV_POST) | BAPI, OData | Original period must be open; reason code required |
| **SAP** | Goods receipt (MIGO) | Reversal movement type (102) | BAPI_GOODSMVT_CREATE / API_GOODSMVT_2 | Partial quantity reversal supported |
| **SAP** | Customer payment | Reset clearing (FBRA) then reverse (FB08) | BAPI | Must reset clearing first — cannot skip |
| **SAP** | Billing document | Cancel billing (VF11) — creates offsetting doc | BAPI_BILLINGDOC_CANCEL | Original must not be cleared/settled |
| **NetSuite** | Invoice/payment | Void (creates reversing journal) | transaction.void() SuiteScript | Requires 'Void Using Reversing Journals' preference disabled for API |
| **NetSuite** | Sales order | Close/cancel line items | SuiteTalk/REST | Fulfilled lines cannot be voided — must create return authorization |
| **NetSuite** | Journal entry | Reverse | N/A (manual or SuiteScript) | Period must be open |
| **NetSuite** | Customer payment | Void or return deposit | transaction.void() | Void vs delete depends on preference setting |
| **D365 Finance** | GL journal | Reverse transaction | OData: /GeneralJournalEntries reversal | Creates offsetting entry, maintains audit trail |
| **D365 Finance** | Vendor payment | Reverse/void check | OData | Unapplies settlement automatically |
| **D365 Finance** | Purchase order receipt | Cancel product receipt | OData | Must cancel before invoice matching |
| **D365 Finance** | Fixed asset transaction | Cancel or Reverse | OData | Cancel = void entirely; Reverse = offsetting entry |

## Compensation Design Patterns

### Forward Recovery vs Backward Recovery

There are two fundamental approaches to handling saga step failures. [src1, src3]

**Backward recovery** (compensating transactions): Undo completed steps in reverse order. This is the standard saga compensation approach. Use when operations are reversible and the business process can be abandoned.

**Forward recovery** (retry + continue): Retry the failed step until it succeeds, then continue forward. Use when the business process must complete (e.g., payroll already initiated, goods already shipped) and the remaining steps are non-optional.

```
BACKWARD RECOVERY (Compensating Transactions)
─────────────────────────────────────────────
Step 1: Create Salesforce Opportunity    ✅ Committed
Step 2: Create SAP Sales Order           ✅ Committed
Step 3: Create NetSuite Invoice          ❌ FAILED
  → Compensate Step 2: Cancel SAP Sales Order
  → Compensate Step 1: Revert SF Opportunity to "Open"

FORWARD RECOVERY (Retry + Continue)
───────────────────────────────────
Step 1: Create Salesforce Opportunity    ✅ Committed
Step 2: Create SAP Sales Order           ✅ Committed
Step 3: Create NetSuite Invoice          ❌ FAILED
  → Retry Step 3 with exponential backoff
  → If retries exhausted: human escalation, but do NOT undo Steps 1-2
```

**Decision rule**: If the process involves irrevocable side effects (payment captured, goods shipped, regulatory filing submitted), use forward recovery for steps after the irrevocable point. Use backward recovery for steps before it.

### Saga Orchestration for ERP Workflows

Orchestration is strongly preferred over choreography for ERP compensation because: (1) ERP APIs have inconsistent event models, (2) compensation ordering requires centralized state, and (3) debugging cross-ERP failures requires a single audit trail. [src2, src3, src7]

```
SAGA ORCHESTRATOR FLOW
──────────────────────
Orchestrator
  │
  ├── Step 1: CreateSalesforceOrder()
  │     ├── onSuccess: record SF Order ID, register compensateStep1()
  │     └── onFailure: → ABORT (nothing to compensate)
  │
  ├── Step 2: CreateSAPSalesOrder(sfOrderId)
  │     ├── onSuccess: record SAP SO number, register compensateStep2()
  │     └── onFailure: → COMPENSATE [Step 1]
  │
  ├── Step 3: CreateNetSuiteInvoice(sapSONumber)
  │     ├── onSuccess: record NS Invoice ID, register compensateStep3()
  │     └── onFailure: → COMPENSATE [Step 2, Step 1]
  │
  └── Step 4: CapturePayment(nsInvoiceId)     ← NON-COMPENSABLE
        ├── onSuccess: → SAGA COMPLETE
        └── onFailure: → COMPENSATE [Step 3, Step 2, Step 1]

COMPENSATION EXECUTION ORDER:
  compensateStep3() → compensateStep2() → compensateStep1()
  (Reverse order of successful execution)
```

## Integration Pattern Decision Tree

```
START — User needs to handle failures in multi-ERP workflows
├── Is the failed operation within a single ERP?
│   ├── YES → Use the ERP's native rollback (SAP LUW, SF trigger rollback)
│   └── NO → Distributed workflow, continue ↓
├── How many systems have already committed?
│   ├── 0 → No compensation needed — just fail and retry
│   ├── 1 → Simple bilateral compensation — reverse the one committed op
│   └── 2+ → Full saga with compensation registry ↓
├── Are all committed operations reversible via API?
│   ├── YES → Backward recovery (compensating transactions)
│   │   ├── < 5 steps? → Inline compensation logic
│   │   └── >= 5 steps? → Use Temporal or Step Functions orchestrator
│   ├── PARTIALLY → Hybrid approach ↓
│   │   ├── Order steps: compensable first, non-compensable last
│   │   ├── For compensable steps → backward recovery
│   │   └── For non-compensable steps → forward recovery (retry)
│   └── NO → Forward recovery only (retry failed step until success)
├── What if compensation itself fails?
│   ├── Financial/regulated → Dead letter queue + manual reconciliation
│   ├── Best-effort → Log failure + alert + scheduled retry
│   └── Can retry indefinitely → Temporal durable execution
└── Orchestration tool?
    ├── Need durability guarantees → Temporal (code-first, replay-safe)
    ├── AWS-native → Step Functions (state machine)
    ├── iPaaS-based → MuleSoft/Boomi/Workato with error handlers
    └── Simple (< 3 steps) → Custom code with compensation array
```

## Quick Reference

### Compensating Actions Per ERP Per Operation Type

| Operation | Salesforce | SAP S/4HANA | NetSuite | D365 Finance |
|---|---|---|---|---|
| **Create record** | DELETE (soft delete) | Reversal document | Void / Delete | Reverse posting |
| **Update record** | PATCH (restore old values) | Correction document | PATCH (restore) | Corrective posting |
| **Delete record** | undelete() (SOAP) | Re-post original | Restore from Recycle Bin | Re-create |
| **Post journal** | N/A (no GL) | FB08 reversal | Reversing journal | Reverse journal |
| **Post invoice** | Credit memo | Cancel billing (VF11) | Credit memo / Void | Credit note |
| **Process payment** | Refund object | Reset + reverse (FBRA+FB08) | Void / Refund | Void check / reverse |
| **Goods receipt** | N/A | Movement type 102 | Item receipt reversal | Cancel product receipt |
| **Close period** | N/A | Non-compensable (reopen) | Non-compensable (reopen) | Non-compensable (reopen) |
| **Send email** | Non-compensable | Non-compensable | Non-compensable | Non-compensable |
| **Print/mail check** | Non-compensable | Non-compensable | Non-compensable | Non-compensable |

## Step-by-Step Integration Guide

### 1. Design your compensation registry

Before writing any saga code, map every step to its compensating action. Register the compensation BEFORE executing the step — this prevents orphaned state when an activity times out mid-execution. [src2]

```typescript
// Compensation registry pattern — register BEFORE execute
interface SagaStep<T> {
  name: string;
  execute: () => Promise<T>;
  compensate: (result: T) => Promise<void>;
}

class CompensationRegistry {
  private completedSteps: Array<{
    name: string;
    compensate: () => Promise<void>;
  }> = [];

  async executeStep<T>(step: SagaStep<T>): Promise<T> {
    // Register compensation BEFORE execution
    // This handles the case where execute() succeeds but
    // the caller crashes before recording success
    const placeholder = { name: step.name, compensate: async () => {} };
    this.completedSteps.unshift(placeholder);

    const result = await step.execute();

    // Update with actual compensation using the result
    placeholder.compensate = () => step.compensate(result);
    return result;
  }

  async compensateAll(): Promise<CompensationReport> {
    const report: CompensationReport = { succeeded: [], failed: [] };
    for (const step of this.completedSteps) {
      try {
        await step.compensate();
        report.succeeded.push(step.name);
      } catch (err) {
        report.failed.push({ name: step.name, error: err });
        // Continue compensating remaining steps — do NOT abort
      }
    }
    return report;
  }
}
```

**Verify**: The registry should contain entries in reverse execution order.

### 2. Define per-ERP compensating actions

Each ERP has different API semantics for undoing operations. Define typed compensating action factories for each ERP. [src4, src5, src6, src8]

```typescript
// Salesforce compensation actions
const salesforceCompensations = {
  deleteRecord: (objectType: string, recordId: string) => async () => {
    // Soft delete — record goes to Recycle Bin (15-day retention)
    await sfClient.delete(`/sobjects/${objectType}/${recordId}`);
  },

  undeleteRecord: (recordId: string) => async () => {
    // Only works for soft-deleted records still in Recycle Bin
    await sfClient.soap('undelete', { ids: [recordId] });
  },

  revertStatus: (objectType: string, id: string, oldStatus: string) => async () => {
    await sfClient.patch(`/sobjects/${objectType}/${id}`, {
      StageName: oldStatus  // or Status, depending on object
    });
  },
};

// SAP S/4HANA compensation actions
const sapCompensations = {
  reverseDocument: (companyCode: string, docNumber: string,
    fiscalYear: string, reasonCode: string) => async () => {
    // FB08 equivalent via BAPI
    await sapClient.call('BAPI_ACC_DOCUMENT_REV_POST', {
      OBJECTKEY: docNumber,
      BUKRS: companyCode,
      GJAHR: fiscalYear,
      REASON_REV: reasonCode, // e.g., '01' = reversal in current period
    });
  },

  reverseGoodsMovement: (materialDoc: string, year: string) => async () => {
    // Movement type 102 (reversal of 101 goods receipt)
    await sapClient.post('/API_GOODSMVT_2/GoodsMovement', {
      GoodsMovementType: '102',
      MaterialDocument: materialDoc,
      MaterialDocumentYear: year,
    });
  },
};

// NetSuite compensation actions
const netsuiteCompensations = {
  voidTransaction: (transactionId: string, tranType: string) => async () => {
    // Note: behavior depends on 'Void Using Reversing Journals' preference
    await nsClient.post('/transaction/void', {
      id: transactionId,
      type: tranType,
    });
  },

  createCreditMemo: (invoiceId: string, amount: number) => async () => {
    await nsClient.post('/record/v1/creditMemo', {
      entity: { id: invoiceId },
      item: [{ amount: -amount }],
    });
  },
};

// Dynamics 365 Finance compensation actions
const d365Compensations = {
  reverseJournal: (journalBatchNumber: string) => async () => {
    await d365Client.post(
      `/GeneralJournalEntries/reverse`,
      { JournalBatchNumber: journalBatchNumber }
    );
  },

  cancelProductReceipt: (receiptNumber: string) => async () => {
    await d365Client.post(
      `/PurchaseOrderProductReceipts/cancel`,
      { ProductReceiptNumber: receiptNumber }
    );
  },
};
```

**Verify**: Each compensating action should be independently testable by creating and then compensating a test record.

### 3. Wire the saga orchestrator

Connect your steps and compensations in an orchestrator. This example uses a Temporal-style workflow, but the pattern applies to any orchestrator. [src2, src7]

```typescript
async function orderToPaySaga(order: OrderRequest): Promise<SagaResult> {
  const registry = new CompensationRegistry();

  try {
    // Step 1: Create Salesforce Opportunity (compensable)
    const sfOppId = await registry.executeStep({
      name: 'createSalesforceOpportunity',
      execute: () => sfClient.createOpportunity(order),
      compensate: (oppId) => salesforceCompensations.revertStatus(
        'Opportunity', oppId, 'Prospecting'
      )(),
    });

    // Step 2: Create SAP Sales Order (compensable)
    const sapSONumber = await registry.executeStep({
      name: 'createSAPSalesOrder',
      execute: () => sapClient.createSalesOrder(order, sfOppId),
      compensate: (soNumber) => sapCompensations.reverseDocument(
        order.companyCode, soNumber, currentFiscalYear(), '01'
      )(),
    });

    // Step 3: Create NetSuite Invoice (compensable)
    const nsInvoiceId = await registry.executeStep({
      name: 'createNetSuiteInvoice',
      execute: () => nsClient.createInvoice(order, sapSONumber),
      compensate: (invId) => netsuiteCompensations.voidTransaction(
        invId, 'invoice'
      )(),
    });

    // Step 4: Capture Payment (NON-COMPENSABLE — execute last)
    await registry.executeStep({
      name: 'capturePayment',
      execute: () => paymentGateway.capture(order.paymentToken, order.amount),
      compensate: async (txnId) => {
        // Payment refund — may take days, flag for manual review
        await paymentGateway.refund(txnId);
        await alertOps('Payment refunded — manual verification required', txnId);
      },
    });

    return { status: 'completed', sfOppId, sapSONumber, nsInvoiceId };

  } catch (error) {
    const report = await registry.compensateAll();

    if (report.failed.length > 0) {
      // Compensation partially failed — escalate to dead letter queue
      await deadLetterQueue.publish({
        sagaId: order.sagaId,
        error: error.message,
        compensationFailures: report.failed,
        remainingActions: report.failed.map(f => f.name),
      });
      await alertOps('Saga compensation partially failed', order.sagaId);
    }

    throw new SagaCompensationError(error, report);
  }
}
```

**Verify**: Test by injecting a failure at each step and confirming all prior steps are compensated.

### 4. Handle compensation failures (compensation of compensation)

When a compensating transaction itself fails, you must not recurse. Instead, route to a dead letter queue for manual or automated retry. [src1, src2]

```typescript
class CompensationFailureHandler {
  private dlq: DeadLetterQueue;
  private maxRetries = 3;

  async handleFailedCompensation(
    sagaId: string,
    failedStep: string,
    compensateAction: () => Promise<void>,
    attempt: number = 1
  ): Promise<void> {
    try {
      await compensateAction();
    } catch (error) {
      if (attempt < this.maxRetries) {
        // Exponential backoff: 2s, 4s, 8s
        await sleep(Math.pow(2, attempt) * 1000);
        return this.handleFailedCompensation(
          sagaId, failedStep, compensateAction, attempt + 1
        );
      }

      // Exhausted retries — dead letter queue
      await this.dlq.publish({
        sagaId,
        failedStep,
        attempts: attempt,
        error: error.message,
        timestamp: new Date().toISOString(),
        requiresManualIntervention: true,
      });

      // DO NOT throw — continue compensating other steps
      console.error(
        `Compensation failed for ${failedStep} after ${attempt} attempts. ` +
        `Routed to DLQ. Saga: ${sagaId}`
      );
    }
  }
}
```

**Verify**: Confirm DLQ receives messages for steps that fail compensation after max retries.

## Code Examples

### Python: Saga Orchestrator with Compensation Registry

```python
# Input:  List of saga steps with execute/compensate callables
# Output: SagaResult with status and compensation report

import asyncio
from dataclasses import dataclass, field
from typing import Any, Callable, Awaitable, Optional

@dataclass
class SagaStep:
    name: str
    execute: Callable[[], Awaitable[Any]]
    compensate: Callable[[Any], Awaitable[None]]

@dataclass
class CompensationResult:
    succeeded: list[str] = field(default_factory=list)
    failed: list[dict] = field(default_factory=list)

class SagaOrchestrator:
    def __init__(self):
        self._completed: list[tuple[str, Callable]] = []

    async def run(self, steps: list[SagaStep]) -> dict[str, Any]:
        results = {}
        try:
            for step in steps:
                result = await step.execute()
                # Register compensation immediately after success
                self._completed.insert(0, (
                    step.name,
                    lambda r=result, s=step: s.compensate(r)
                ))
                results[step.name] = result
            return {"status": "completed", "results": results}
        except Exception as e:
            report = await self._compensate_all()
            return {
                "status": "compensated",
                "error": str(e),
                "compensation_report": report,
            }

    async def _compensate_all(self) -> CompensationResult:
        report = CompensationResult()
        for name, compensate_fn in self._completed:
            try:
                await compensate_fn()
                report.succeeded.append(name)
            except Exception as e:
                report.failed.append({"step": name, "error": str(e)})
                # Continue — never abort compensation midway
        return report


# Usage example for cross-ERP order workflow
async def order_saga(order):
    orchestrator = SagaOrchestrator()

    steps = [
        SagaStep(
            name="create_sf_opportunity",
            execute=lambda: sf_client.create_opportunity(order),
            compensate=lambda opp_id: sf_client.revert_stage(opp_id, "Prospecting"),
        ),
        SagaStep(
            name="create_sap_sales_order",
            execute=lambda: sap_client.create_sales_order(order),
            compensate=lambda so_num: sap_client.reverse_document(so_num),
        ),
        SagaStep(
            name="create_netsuite_invoice",
            execute=lambda: ns_client.create_invoice(order),
            compensate=lambda inv_id: ns_client.void_transaction(inv_id),
        ),
    ]

    return await orchestrator.run(steps)
```

### JavaScript/Node.js: Temporal Workflow with Compensation

```javascript
// Input:  Order details from upstream system
// Output: Completed saga or compensated state with audit trail

// Using Temporal SDK for durable execution
import { proxyActivities, sleep } from '@temporalio/workflow';

const { createSFOpportunity, cancelSFOpportunity,
        createSAPSalesOrder, reverseSAPDocument,
        createNSInvoice, voidNSTransaction,
        capturePayment, refundPayment } = proxyActivities({
  startToCloseTimeout: '30s',
  retry: { maximumAttempts: 3 },
});

export async function orderToPayWorkflow(order) {
  const compensations = [];

  try {
    // Step 1 — compensable
    compensations.unshift(() => cancelSFOpportunity(sfOppId));
    const sfOppId = await createSFOpportunity(order);

    // Step 2 — compensable
    compensations.unshift(() => reverseSAPDocument(sapSONum));
    const sapSONum = await createSAPSalesOrder(order, sfOppId);

    // Step 3 — compensable
    compensations.unshift(() => voidNSTransaction(nsInvId));
    const nsInvId = await createNSInvoice(order, sapSONum);

    // Step 4 — non-compensable (payment) — execute last
    compensations.unshift(() => refundPayment(paymentTxnId));
    const paymentTxnId = await capturePayment(order);

    return { status: 'completed', sfOppId, sapSONum, nsInvId };
  } catch (err) {
    // Execute compensations in reverse order
    for (const compensate of compensations) {
      try {
        await compensate();
      } catch (compErr) {
        // Log but continue — do not abort compensation chain
        console.error('Compensation failed:', compErr.message);
      }
    }
    throw err;
  }
}
```

## Error Handling & Failure Points

### Common Error Scenarios

| Scenario | Cause | Impact | Resolution |
|---|---|---|---|
| Compensation blocked by closed period | SAP/NetSuite fiscal period closed | Reversal document cannot be posted | Post reversal in current period with cross-period reference |
| Partial compensation success | Network timeout during compensation chain | System in inconsistent state across ERPs | DLQ + scheduled reconciliation job |
| Idempotency violation | Compensation executed twice due to retry | Double-reversal creates phantom transactions | Use idempotency keys per compensation action |
| Rate limit hit during compensation | Multiple compensations trigger ERP rate limiting | Remaining compensations delayed or blocked | Exponential backoff; space compensation calls |
| Record locked by another user | Concurrent ERP user editing same record | Compensation action fails with lock error | Retry with jitter (random delay 1-5s) |
| Non-compensable step already executed | Email sent, check printed, goods shipped | Cannot undo physical-world actions | Send correction notification; flag for manual handling |
| Recycle Bin expired (Salesforce) | 15-day retention passed before compensation | Cannot undelete the record | Re-create the record with original data from audit log |
| Authorization expired mid-compensation | Long-running saga outlives auth token | 401 errors during compensation | Refresh token before each compensation step |

### Failure Points in Production

- **Compensation ordering matters**: If Step 3 depends on Step 2's data, compensating Step 2 before Step 3 may fail. Always compensate in reverse execution order, not arbitrary order. [src1]
- **SAP period-end blocking**: Reversal documents require open posting periods. If the fiscal period closes between the original posting and the compensation attempt, the reversal is blocked. Fix: `Post to current period with reason code indicating cross-period reversal, or use SAP special period`. [src4]
- **NetSuite preference conflict**: The `transaction.void()` SuiteScript function throws an error if the "Void Transactions Using Reversing Journals" preference is enabled. Fix: `Use SuiteScript to create a reversing journal entry manually instead of calling void()`. [src5]
- **Salesforce governor limits during compensation**: A saga compensating 200+ records in a single Apex transaction hits DML limits (150 statements). Fix: `Batch compensations into groups of 150 or use Bulk API for mass compensation`. [src8]
- **D365 settlement reversal dependency**: Cannot reverse a vendor payment until the settlement (application to invoice) is first reversed. Fix: `Always unapply settlements before reversing payments — two-step compensation`. [src6]
- **Temporal workflow timeout kills compensation**: If a Temporal workflow timeout is set too short, the compensation chain may be interrupted. Fix: `Set workflow timeouts generously or use child workflows for compensation`. [src2]

## Anti-Patterns

### Wrong: Ignoring compensation — assuming failures are rare

```typescript
// BAD — no compensation logic, relies on "it usually works"
async function naiveOrderFlow(order) {
  const sfId = await createSalesforceOpp(order);
  const sapSO = await createSAPOrder(order, sfId);     // What if this fails?
  const nsInv = await createNetSuiteInvoice(order, sapSO); // sfId is orphaned
  return { sfId, sapSO, nsInv };
}
// Result: Salesforce has an Opportunity with no matching SAP order
// Discovered weeks later during reconciliation — $200/hr to fix manually
```

### Correct: Every step has a registered compensation

```typescript
// GOOD — every step has a compensating action
async function compensatedOrderFlow(order) {
  const registry = new CompensationRegistry();
  try {
    const sfId = await registry.executeStep({
      name: 'sf_opp', execute: () => createSalesforceOpp(order),
      compensate: (id) => deleteSalesforceOpp(id),
    });
    const sapSO = await registry.executeStep({
      name: 'sap_so', execute: () => createSAPOrder(order, sfId),
      compensate: (so) => reverseSAPDocument(so),
    });
    const nsInv = await registry.executeStep({
      name: 'ns_inv', execute: () => createNetSuiteInvoice(order, sapSO),
      compensate: (inv) => voidNetSuiteTransaction(inv),
    });
    return { sfId, sapSO, nsInv };
  } catch (e) {
    await registry.compensateAll();
    throw e;
  }
}
```

### Wrong: Assuming ERP API operations are idempotent

```typescript
// BAD — retrying compensation without idempotency key
async function retryCompensation(action) {
  for (let i = 0; i < 3; i++) {
    try { await action(); return; }
    catch (e) { /* retry */ }
  }
}
// Result: SAP creates 3 reversal documents for 1 original document
// GL is now triple-reversed (net = 1 unreversed original + 2 extra reversals)
```

### Correct: Idempotent compensation with tracking

```typescript
// GOOD — idempotency key prevents duplicate compensations
async function idempotentCompensation(
  sagaId: string, stepName: string, action: () => Promise<void>
) {
  const idempotencyKey = `${sagaId}:${stepName}:compensate`;

  // Check if already compensated
  const existing = await compensationLog.find(idempotencyKey);
  if (existing?.status === 'completed') return; // Already done

  await compensationLog.upsert(idempotencyKey, { status: 'in_progress' });
  try {
    await action();
    await compensationLog.upsert(idempotencyKey, { status: 'completed' });
  } catch (error) {
    await compensationLog.upsert(idempotencyKey, {
      status: 'failed', error: error.message
    });
    throw error;
  }
}
```

### Wrong: Putting non-compensable steps early in the saga

```typescript
// BAD — payment captured at Step 2, but Step 3 can fail
async function badStepOrdering(order) {
  const sfId = await createSalesforceOpp(order);        // Step 1
  const paymentId = await capturePayment(order);         // Step 2 — NON-COMPENSABLE
  const sapSO = await createSAPOrder(order, sfId);       // Step 3 — if this fails...
  // ...you already captured payment but have no SAP order. Refund takes 5-10 days.
}
```

### Correct: Non-compensable steps execute last

```typescript
// GOOD — non-compensable payment capture is the final step
async function correctStepOrdering(order) {
  const sfId = await createSalesforceOpp(order);         // Step 1 — compensable
  const sapSO = await createSAPOrder(order, sfId);       // Step 2 — compensable
  const nsInv = await createNetSuiteInvoice(order, sapSO); // Step 3 — compensable
  const paymentId = await capturePayment(order);         // Step 4 — NON-COMPENSABLE (last)
}
```

## Common Pitfalls

- **Audit trail for compensated transactions**: Compensated transactions must leave a complete audit trail showing the original operation, the failure, and the compensation. Regulatory requirements (SOX, IFRS) mandate that reversed financial transactions remain visible. Fix: `Log every compensation action with original transaction ID, timestamp, reason, and compensating transaction ID in a dedicated saga_audit_log table`. [src1]
- **Assuming void and reverse are the same**: In NetSuite, voiding a transaction removes its GL impact entirely (as if it never happened), while reversing creates an offsetting journal entry. SOX compliance typically requires reversal, not void. Fix: `Use reversing journals for financial transactions in regulated environments; reserve void for non-financial records`. [src5]
- **Compensation timing assumptions**: SAP reversal documents must be posted in an open fiscal period. If your saga runs during month-end close and compensation is triggered after the period closes, the reversal is blocked. Fix: `Check period status before attempting SAP reversals; if closed, post to current period with cross-period reason code`. [src4]
- **Ignoring concurrent modifications**: Between the original operation and the compensation attempt, another user or process may have modified the record. A blind "restore original values" compensation overwrites their changes. Fix: `Use conditional updates (ETags, version fields, SystemModstamp) to detect concurrent modifications before compensating`. [src1]
- **Not testing compensation paths**: Teams test the happy path exhaustively but never test compensations. The first real compensation attempt fails in production. Fix: `Include compensation testing in CI — inject failures at each saga step and verify full compensation completes`. [src2]
- **Recursive compensation**: When compensation fails, some teams try to "compensate the compensation." This creates infinite recursion. Fix: `Cap at one compensation attempt per step. If compensation fails after retries, route to DLQ for manual resolution — never nest compensations`. [src1, src7]

## Diagnostic Commands

```bash
# Check SAP posting period status (must be open for reversals)
curl -X GET "$SAP_BASE_URL/API_FISCALYEARPERIOD_SRV/FiscalYearPeriods?\
$filter=FiscalYear eq '2026' and FiscalPeriod eq '03'" \
  -H "Authorization: Bearer $SAP_TOKEN"

# Salesforce: check if record is in Recycle Bin (available for undelete)
curl "$SF_INSTANCE/services/data/v62.0/queryAll/?q=\
SELECT+Id,IsDeleted+FROM+Opportunity+WHERE+Id='$OPP_ID'" \
  -H "Authorization: Bearer $SF_TOKEN"

# NetSuite: check Void Using Reversing Journals preference
# (SuiteScript — no direct REST endpoint for preferences)
# Check via: Setup > Accounting > Accounting Preferences > General tab

# D365 Finance: check if journal can be reversed
curl -X GET "$D365_BASE_URL/data/GeneralJournalEntries?\
\$filter=JournalBatchNumber eq '$JOURNAL_BATCH'" \
  -H "Authorization: Bearer $D365_TOKEN"

# Check dead letter queue for failed compensations
# (implementation-specific — example for AWS SQS DLQ)
aws sqs get-queue-attributes \
  --queue-url "$DLQ_URL" \
  --attribute-names ApproximateNumberOfMessages

# Temporal: check workflow compensation status
tctl workflow show -w "$SAGA_WORKFLOW_ID" -r "$RUN_ID"
```

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Multi-ERP workflow with 3+ steps that must be consistent | Single-ERP operation with native transaction support | ERP's built-in transaction/rollback mechanism |
| Business process spans Salesforce + SAP + NetSuite + payment | Simple A-to-B sync between two systems | Direct API call with retry + dead letter queue |
| Regulatory requirement for complete audit trail of reversals | Read-only data extraction or reporting integration | CDC or batch export — no compensation needed |
| Operations are mostly reversible via ERP APIs | Most operations are non-compensable (email, physical shipment) | Forward recovery with retry until success |
| Failure rate is >0.1% and manual cleanup is too expensive | Failure rate is negligible and manual fix is acceptable | Scheduled reconciliation job (cheaper to build) |

## Cross-System Comparison

| Capability | Salesforce | SAP S/4HANA | NetSuite | D365 Finance |
|---|---|---|---|---|
| **Record deletion reversal** | Undelete from Recycle Bin (15 days) | Re-post original document | Restore from Recycle Bin (limited) | Re-create record |
| **Financial reversal** | Credit memo (not true reversal) | Reversal document (FB08) — native | Void or reversing journal | Reverse transaction — native |
| **Reversal API support** | REST DELETE + SOAP undelete | BAPI + OData (comprehensive) | SuiteScript void() + REST | OData reversal endpoints |
| **Period dependency** | N/A (no fiscal periods in CRM) | Must be in open period | Must be in open period | Must be in open period |
| **Partial reversal** | Per-line-item possible | Partial quantity reversal (MIGO) | Per-line credit memo | Per-line corrective posting |
| **Reversal audit trail** | Recycle Bin + field history | Reversal document linked to original | Void memo on transaction | Reversing entry linked to original |
| **Bulk compensation** | Bulk API 2.0 delete | Mass reversal (F.80) | CSV import for mass void | Batch journal reversal |
| **Idempotent reversals** | No (must check status first) | No (must check if already reversed) | No (double-void throws error) | No (must check reversal status) |

## Important Caveats

- ERPs update their reversal APIs with each release. SAP's API_GOODSMVT_2 replaced older BAPIs in 2023; always verify the current recommended API for your S/4HANA version.
- NetSuite's `transaction.void()` behavior fundamentally changes based on a single account-level preference. Test in sandbox with the same preference settings as production.
- Salesforce Recycle Bin has a 15-day retention limit and a 25x storage limit (25 times the number of records your license allows). High-volume orgs may hit the Recycle Bin limit, making undelete impossible.
- Financial compensations in regulated industries (SOX, IFRS, GAAP) require specific reversal documentation. A simple API delete is not compliant — you must create offsetting entries with audit-linkable references.
- Temporal and Step Functions provide durable execution guarantees, but they do not make ERP APIs themselves more reliable. The ERP is still the bottleneck.
- Compensation timing is critical: the longer the delay between original operation and compensation, the higher the risk of concurrent modifications, period closings, and dependent downstream processes.

## Related Units

- [Saga Pattern for ERP Distributed Transactions](/business/erp-integration/saga-pattern-erp-transactions/2026)
- [Error Handling and Dead Letter Queues](/business/erp-integration/error-handling-dead-letter-queues/2026)
- [Idempotency Patterns for ERP Integrations](/business/erp-integration/idempotency-erp-integrations/2026)
- [Order-to-Cash Integration](/business/erp-integration/order-to-cash-integration/2026)
- [Procure-to-Pay Integration](/business/erp-integration/procure-to-pay-integration/2026)
- [Change Data Capture for ERP](/business/erp-integration/change-data-capture-erp/2026)
