Compensating Transaction Pattern
Undo the effects of a completed transaction by executing a counter-operation, enabling eventual consistency in long-running business processes across distributed services.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Compensating Transaction Pattern
Overview
The Compensating Transaction Pattern undoes the effects of a completed business operation by executing a semantic counter-operation. Unlike database rollback (which undoes uncommitted changes), compensating transactions undo operations that have already been committed to external systems — payments that have been charged, inventory that has been reserved, or emails that have been sent.
In distributed systems, ACID transactions across services are impractical. The Saga Pattern coordinates a sequence of local transactions, and when one step fails, compensating transactions roll back previously completed steps. This enables long-running business processes to maintain eventual consistency without distributed locks or two-phase commit.
When to Use
Use the Compensating Transaction Pattern when:
- A business process spans multiple distributed services or databases
- You need to undo operations that have already been committed externally
- Two-phase commit (2PC) is unavailable or impractical (most microservice architectures)
- Long-running processes (seconds to days) need failure recovery semantics
When to Avoid
- The operation is within a single database and a simple transaction rollback works
- Compensating logic is impossible (e.g., an email already sent to a customer)
- The business process is so short that distributed transactions are acceptable
- Compensating transactions would themselves fail, creating an unrecoverable state
Solution
Python
from dataclasses import dataclass
from typing import List, Callable, Optional
from datetime import datetime
import uuid
@dataclass
class StepResult:
success: bool
step_name: str
compensation_needed: bool = False
compensation_error: Optional[str] = None
class SagaOrchestrator:
"""Coordinates a saga with compensating transactions"""
def __init__(self):
self.completed_steps: List[dict] = []
self.compensation_log: List[dict] = []
def execute(self, steps: List[dict]) -> StepResult:
"""
steps: list of dicts with 'name', 'action', 'compensate'
Each is a callable that returns success boolean
"""
for i, step in enumerate(steps):
print(f"Executing step {i+1}: {step['name']}")
success = step['action']()
if success:
self.completed_steps.append({
"index": i,
"name": step["name"],
"compensate": step["compensate"]
})
else:
print(f"Step {step['name']} failed! Running compensating transactions...")
self._compensate()
return StepResult(success=False, step_name=step["name"])
return StepResult(success=True, step_name="all_steps")
def _compensate(self):
"""Run compensating transactions in reverse order"""
for step in reversed(self.completed_steps):
print(f"Compensating: {step['name']}")
try:
step["compensate"]()
self.compensation_log.append({
"step": step["name"],
"status": "success",
"timestamp": datetime.now().isoformat()
})
except Exception as e:
self.compensation_log.append({
"step": step["name"],
"status": "failed",
"error": str(e),
"timestamp": datetime.now().isoformat()
})
print(f"WARNING: Compensation failed for {step['name']}: {e}")
# ============================================================================
# DOMAIN SERVICES WITH COMPENSATION
# ============================================================================
class PaymentService:
def __init__(self):
self.charges = {}
def charge(self, order_id: str, amount: float) -> bool:
txn_id = str(uuid.uuid4())
self.charges[order_id] = {"txn_id": txn_id, "amount": amount, "status": "charged"}
print(f" [Payment] Charged ${amount} for order {order_id}, txn={txn_id}")
return True
def refund(self, order_id: str) -> bool:
charge = self.charges.get(order_id)
if charge:
charge["status"] = "refunded"
print(f" [Payment] Refunded ${charge['amount']} for order {order_id}")
return True
print(f" [Payment] No charge found for order {order_id}")
return False
class InventoryService:
def __init__(self):
self.stock = {"SKU-001": 100, "SKU-002": 50}
self.reservations = {}
def reserve(self, order_id: str, sku: str, qty: int) -> bool:
if self.stock.get(sku, 0) >= qty:
self.stock[sku] -= qty
self.reservations[order_id] = {"sku": sku, "qty": qty}
print(f" [Inventory] Reserved {qty}x {sku} for order {order_id}")
return True
print(f" [Inventory] Insufficient stock for {sku}")
return False
def release(self, order_id: str) -> bool:
reservation = self.reservations.pop(order_id, None)
if reservation:
self.stock[reservation["sku"]] += reservation["qty"]
print(f" [Inventory] Released {reservation['qty']}x {reservation['sku']}")
return True
return False
class ShippingService:
def __init__(self):
self.shipments = {}
def create_label(self, order_id: str, address: str) -> bool:
self.shipments[order_id] = {"address": address, "status": "label_created"}
print(f" [Shipping] Label created for order {order_id}")
return True
def cancel_label(self, order_id: str) -> bool:
shipment = self.shipments.pop(order_id, None)
if shipment:
print(f" [Shipping] Label cancelled for order {order_id}")
return True
return False
# ============================================================================
# SAGA DEFINITION
# ============================================================================
class OrderSaga:
def __init__(self, payments: PaymentService, inventory: InventoryService,
shipping: ShippingService):
self.payments = payments
self.inventory = inventory
self.shipping = shipping
def create_order(self, order_id: str, amount: float, sku: str, qty: int,
address: str) -> StepResult:
saga = SagaOrchestrator()
steps = [
{
"name": "charge_payment",
"action": lambda: self.payments.charge(order_id, amount),
"compensate": lambda: self.payments.refund(order_id)
},
{
"name": "reserve_inventory",
"action": lambda: self.inventory.reserve(order_id, sku, qty),
"compensate": lambda: self.inventory.release(order_id)
},
{
"name": "create_shipping_label",
"action": lambda: self.shipping.create_label(order_id, address),
"compensate": lambda: self.shipping.cancel_label(order_id)
}
]
return saga.execute(steps)
# ============================================================================
# USAGE
# ============================================================================
payments = PaymentService()
inventory = InventoryService()
shipping = ShippingService()
saga = OrderSaga(payments, inventory, shipping)
# Successful order
print("=== ORDER 1 (Success) ===")
result = saga.create_order("ORD-001", 99.99, "SKU-001", 2, "123 Main St")
print(f"Result: {'SUCCESS' if result.success else 'FAILED'}")
# Failed order (insufficient stock triggers compensation)
print("\n=== ORDER 2 (Failure -> Compensation) ===")
result = saga.create_order("ORD-002", 999.99, "SKU-999", 500, "456 Oak Ave")
print(f"Result: {'SUCCESS' if result.success else 'FAILED'}")
print(f"Payment refunded: {payments.charges.get('ORD-002', {}).get('status')}")
Java
import java.util.*;
import java.util.function.*;
// Domain services
class PaymentService {
private final Map<String, Map<String, Object>> charges = new HashMap<>();
public boolean charge(String orderId, double amount) {
Map<String, Object> charge = new HashMap<>();
charge.put("amount", amount);
charge.put("status", "charged");
charges.put(orderId, charge);
System.out.println(" [Payment] Charged $" + amount + " for " + orderId);
return true;
}
public boolean refund(String orderId) {
Map<String, Object> charge = charges.get(orderId);
if (charge != null) {
charge.put("status", "refunded");
System.out.println(" [Payment] Refunded $" + charge.get("amount") + " for " + orderId);
return true;
}
return false;
}
}
class InventoryService {
private final Map<String, Integer> stock = new HashMap<>(Map.of("SKU-001", 100));
private final Map<String, Map<String, Object>> reservations = new HashMap<>();
public boolean reserve(String orderId, String sku, int qty) {
int available = stock.getOrDefault(sku, 0);
if (available >= qty) {
stock.put(sku, available - qty);
Map<String, Object> res = new HashMap<>();
res.put("sku", sku); res.put("qty", qty);
reservations.put(orderId, res);
System.out.println(" [Inventory] Reserved " + qty + "x " + sku);
return true;
}
System.out.println(" [Inventory] Insufficient stock for " + sku);
return false;
}
public boolean release(String orderId) {
Map<String, Object> res = reservations.remove(orderId);
if (res != null) {
String sku = (String) res.get("sku");
int qty = (Integer) res.get("qty");
stock.put(sku, stock.get(sku) + qty);
System.out.println(" [Inventory] Released " + qty + "x " + sku);
return true;
}
return false;
}
}
// Saga step
class SagaStep {
final String name;
final Supplier<Boolean> action;
final Runnable compensate;
SagaStep(String name, Supplier<Boolean> action, Runnable compensate) {
this.name = name; this.action = action; this.compensate = compensate;
}
}
// Saga orchestrator
class SagaOrchestrator {
private final List<SagaStep> completedSteps = new ArrayList<>();
public boolean execute(List<SagaStep> steps) {
for (SagaStep step : steps) {
System.out.println("Executing: " + step.name);
if (step.action.get()) {
completedSteps.add(step);
} else {
System.out.println(step.name + " failed! Compensating...");
compensate();
return false;
}
}
return true;
}
private void compensate() {
List<SagaStep> reverse = new ArrayList<>(completedSteps);
Collections.reverse(reverse);
for (SagaStep step : reverse) {
System.out.println("Compensating: " + step.name);
try {
step.compensate.run();
} catch (Exception e) {
System.err.println("WARNING: Compensation failed for " + step.name + ": " + e.getMessage());
}
}
}
}
// Usage
PaymentService payments = new PaymentService();
InventoryService inventory = new InventoryService();
SagaOrchestrator saga = new SagaOrchestrator();
List<SagaStep> steps = List.of(
new SagaStep("charge", () -> payments.charge("ORD-001", 99.99), () -> payments.refund("ORD-001")),
new SagaStep("reserve", () -> inventory.reserve("ORD-001", "SKU-001", 2), () -> inventory.release("ORD-001"))
);
boolean success = saga.execute(steps);
System.out.println("Saga result: " + (success ? "SUCCESS" : "FAILED"));
JavaScript
class PaymentService {
constructor() {
this.charges = new Map();
}
charge(orderId, amount) {
this.charges.set(orderId, { amount, status: 'charged' });
console.log(` [Payment] Charged $${amount} for ${orderId}`);
return true;
}
refund(orderId) {
const charge = this.charges.get(orderId);
if (charge) {
charge.status = 'refunded';
console.log(` [Payment] Refunded $${charge.amount} for ${orderId}`);
return true;
}
return false;
}
}
class InventoryService {
constructor() {
this.stock = new Map([['SKU-001', 100]]);
this.reservations = new Map();
}
reserve(orderId, sku, qty) {
const available = this.stock.get(sku) || 0;
if (available >= qty) {
this.stock.set(sku, available - qty);
this.reservations.set(orderId, { sku, qty });
console.log(` [Inventory] Reserved ${qty}x ${sku}`);
return true;
}
console.log(` [Inventory] Insufficient stock for ${sku}`);
return false;
}
release(orderId) {
const res = this.reservations.get(orderId);
if (res) {
this.stock.set(res.sku, this.stock.get(res.sku) + res.qty);
console.log(` [Inventory] Released ${res.qty}x ${res.sku}`);
this.reservations.delete(orderId);
return true;
}
return false;
}
}
class SagaOrchestrator {
constructor() {
this.completedSteps = [];
}
async execute(steps) {
for (const step of steps) {
console.log(`Executing: ${step.name}`);
const success = await step.action();
if (success) {
this.completedSteps.push(step);
} else {
console.log(`${step.name} failed! Compensating...`);
await this.compensate();
return { success: false, failedStep: step.name };
}
}
return { success: true };
}
async compensate() {
const reverse = [...this.completedSteps].reverse();
for (const step of reverse) {
console.log(`Compensating: ${step.name}`);
try {
await step.compensate();
} catch (e) {
console.error(`WARNING: Compensation failed for ${step.name}: ${e.message}`);
}
}
}
}
// Usage
async function demo() {
const payments = new PaymentService();
const inventory = new InventoryService();
const saga = new SagaOrchestrator();
const steps = [
{
name: 'charge',
action: () => payments.charge('ORD-001', 99.99),
compensate: () => payments.refund('ORD-001')
},
{
name: 'reserve',
action: () => inventory.reserve('ORD-001', 'SKU-001', 2),
compensate: () => inventory.release('ORD-001')
}
];
const result = await saga.execute(steps);
console.log('Result:', result.success ? 'SUCCESS' : 'FAILED');
}
demo().catch(console.error);
Explanation
A compensating transaction is a semantic undo rather than a database rollback:
- Charge payment → compensation is refund payment
- Reserve inventory → compensation is release inventory
- Create shipping label → compensation is cancel shipping label
The Saga orchestrator executes steps sequentially. If any step fails, it runs compensations in reverse order for all previously completed steps. This ensures the system returns to a consistent state, even though individual operations were already committed.
Key properties:
- Compensations are themselves business operations, not database commands
- Compensations may fail (e.g., a refund rejected by the payment processor) and must be monitored
- The saga log records what happened for audit and manual intervention
Variants
| Variant | Coordination | Use Case |
|---|---|---|
| Orchestrated Saga | Central coordinator manages steps and compensations | Complex workflows with clear ordering |
| Choreographed Saga | Services emit events; listeners trigger next steps or compensations | Decoupled, event-driven architectures |
| Parallel Saga | Independent steps run concurrently; compensations run for all on failure | High-throughput, loosely coupled steps |
| Nested Saga | A saga step is itself a sub-saga with its own compensations | Recursive business processes |
Best Practices
- Design compensations upfront. They are harder to retrofit than the original operations.
- Make compensations idempotent. They may be retried if the first attempt fails.
- Log everything. Saga state, compensation results, and failures must be observable.
- Set timeouts. A step that hangs forever blocks the entire saga.
- Provide manual intervention hooks. Some compensations require human approval (e.g., refunds over a threshold).
Common Mistakes
- Assuming compensations always succeed. Payment refunds can be rejected; inventory may already be shipped.
- Missing compensation for a step. Every saga step must have a defined counter-operation.
- Non-idempotent compensations. Running a compensation twice should not double-refund.
- Losing saga state. If the orchestrator crashes, in-flight sagas must be recoverable from a persistent log.
- Ignoring partial failures. A step that “half succeeds” (e.g., payment charged but not recorded) is the hardest case to compensate.
Real-World Examples
E-Commerce Order Processing
Placing an order involves payment, inventory reservation, and shipping. If shipping fails after payment succeeds, the saga compensates by refunding the payment and releasing inventory.
Travel Booking
Booking a trip involves flights, hotels, and car rentals. If the hotel booking fails after the flight is booked, the saga cancels the flight reservation (if possible) and refunds the customer.
Banking Transfers
An inter-bank transfer saga debits the source account, initiates a wire, and credits the destination. If the wire fails, the saga credits the source account back (compensating the debit).
Frequently Asked Questions
Q: What is the difference between compensating transaction and database rollback? A: Rollback undoes uncommitted changes within a single database transaction. Compensation undoes committed changes across distributed systems by executing counter-business-operations.
Q: Can all operations be compensated? A: No. Some operations are irreversible (e.g., an email sent, a physical item shipped). These require alternative strategies: retries, human intervention, or accepting the inconsistency.
Q: How does this relate to the Saga Pattern? A: Compensating Transaction is the mechanism used by Saga to achieve rollback in distributed systems. Saga is the coordination strategy; Compensation is the undo mechanism.
Q: What if a compensation itself fails? A: Log the failure, alert operations, and potentially retry. Some systems maintain a “compensation queue” that retries failed compensations with exponential backoff until resolved or manually handled.