Patrón Compensating Transaction
Deshace los efectos de una transacción completada ejecutando una operación contraria, habilitando consistencia eventual en procesos distribuidos de larga duración.
Descripción General
El Patrón Compensating Transaction deshace los efectos de una operación de negocio completada ejecutando una operación contraria semántica. A diferencia del rollback de base de datos (que deshace cambios no commiteados), las transacciones compensatorias deshacen operaciones que ya fueron commiteadas a sistemas externos — pagos que fueron cobrados, inventario que fue reservado, o emails que fueron enviados.
En sistemas distribuidos, las transacciones ACID a través de servicios son impracticables. El Patrón Saga coordina una secuencia de transacciones locales, y cuando un paso falla, las transacciones compensatorias revierten los pasos previamente completados. Esto permite que procesos de negocio de larga duración mantengan consistencia eventual sin locks distribuidos o two-phase commit.
Cuándo Usar
Usa el Patrón Compensating Transaction cuando:
- Un proceso de negocio abarca múltiples servicios o bases de datos distribuidos
- Necesitas deshacer operaciones que ya fueron commiteadas externamente
- El two-phase commit (2PC) no está disponible o es impracticable (la mayoría de las arquitecturas de microservicios)
- Los procesos de larga duración (segundos a días) necesitan semánticas de recuperación por fallas
Cuándo Evitar
- La operación está dentro de una única base de datos y un simple rollback de transacción funciona
- La lógica de compensación es imposible (ej. un email ya enviado a un cliente)
- El proceso de negocio es tan corto que las transacciones distribuidas son aceptables
- Las transacciones compensatorias mismas fallarían, creando un estado irrecuperable
Solución
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:
"""Coordina una saga con transacciones compensatorias"""
def __init__(self):
self.completed_steps: List[dict] = []
self.compensation_log: List[dict] = []
def execute(self, steps: List[dict]) -> StepResult:
"""
steps: lista de dicts con 'name', 'action', 'compensate'
Cada uno es un callable que retorna booleano de éxito
"""
for i, step in enumerate(steps):
print(f"Ejecutando paso {i+1}: {step['name']}")
success = step['action']()
if success:
self.completed_steps.append({
"index": i,
"name": step["name"],
"compensate": step["compensate"]
})
else:
print(f"Paso {step['name']} falló! Ejecutando transacciones compensatorias...")
self._compensate()
return StepResult(success=False, step_name=step["name"])
return StepResult(success=True, step_name="all_steps")
def _compensate(self):
"""Ejecuta compensaciones en orden inverso"""
for step in reversed(self.completed_steps):
print(f"Compensando: {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"ADVERTENCIA: Compensación falló para {step['name']}: {e}")
# ============================================================================
# SERVICIOS DE DOMINIO CON COMPENSACIÓN
# ============================================================================
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] Cobrado ${amount} para orden {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] Reembolsado ${charge['amount']} para orden {order_id}")
return True
print(f" [Payment] No se encontró cargo para orden {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] Reservado {qty}x {sku} para orden {order_id}")
return True
print(f" [Inventory] Stock insuficiente para {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] Liberado {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] Etiqueta creada para orden {order_id}")
return True
def cancel_label(self, order_id: str) -> bool:
shipment = self.shipments.pop(order_id, None)
if shipment:
print(f" [Shipping] Etiqueta cancelada para orden {order_id}")
return True
return False
# ============================================================================
# DEFINICIÓN DE SAGA
# ============================================================================
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)
# ============================================================================
# USO
# ============================================================================
payments = PaymentService()
inventory = InventoryService()
shipping = ShippingService()
saga = OrderSaga(payments, inventory, shipping)
# Orden exitosa
print("=== ORDEN 1 (Éxito) ===")
result = saga.create_order("ORD-001", 99.99, "SKU-001", 2, "123 Main St")
print(f"Resultado: {'ÉXITO' if result.success else 'FALLIDO'}")
# Orden fallida (stock insuficiente dispara compensación)
print("\n=== ORDEN 2 (Fallo -> Compensación) ===")
result = saga.create_order("ORD-002", 999.99, "SKU-999", 500, "456 Oak Ave")
print(f"Resultado: {'ÉXITO' if result.success else 'FALLIDO'}")
print(f"Pago reembolsado: {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] Cobrado $" + amount + " para " + 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] Reembolsado $" + charge.get("amount") + " para " + 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] Reservado " + qty + "x " + sku);
return true;
}
System.out.println(" [Inventory] Stock insuficiente para " + 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] Liberado " + 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("Ejecutando: " + step.name);
if (step.action.get()) {
completedSteps.add(step);
} else {
System.out.println(step.name + " falló! Compensando...");
compensate();
return false;
}
}
return true;
}
private void compensate() {
List<SagaStep> reverse = new ArrayList<>(completedSteps);
Collections.reverse(reverse);
for (SagaStep step : reverse) {
System.out.println("Compensando: " + step.name);
try {
step.compensate.run();
} catch (Exception e) {
System.err.println("ADVERTENCIA: Compensación falló para " + step.name + ": " + e.getMessage());
}
}
}
}
// Uso — pon esta clase en CompensatingTransactionDemo.java
public class CompensatingTransactionDemo {
public static void main(String[] args) {
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("Resultado de saga: " + (success ? "ÉXITO" : "FALLIDO"));
}
}
JavaScript
class PaymentService {
constructor() {
this.charges = new Map();
}
charge(orderId, amount) {
this.charges.set(orderId, { amount, status: 'charged' });
console.log(` [Payment] Cobrado $${amount} para ${orderId}`);
return true;
}
refund(orderId) {
const charge = this.charges.get(orderId);
if (charge) {
charge.status = 'refunded';
console.log(` [Payment] Reembolsado $${charge.amount} para ${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] Reservado ${qty}x ${sku}`);
return true;
}
console.log(` [Inventory] Stock insuficiente para ${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] Liberado ${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(`Ejecutando: ${step.name}`);
const success = await step.action();
if (success) {
this.completedSteps.push(step);
} else {
console.log(`${step.name} falló! Compensando...`);
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(`Compensando: ${step.name}`);
try {
await step.compensate();
} catch (e) {
console.error(`ADVERTENCIA: Compensación falló para ${step.name}: ${e.message}`);
}
}
}
}
// Uso
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('Resultado:', result.success ? 'ÉXITO' : 'FALLIDO');
}
demo().catch(console.error);
Explicación
Una transacción compensatoria es un deshacer semántico en lugar de un rollback de base de datos:
- Cobrar pago → compensación es reembolsar pago
- Reservar inventario → compensación es liberar inventario
- Crear etiqueta de envío → compensación es cancelar etiqueta de envío
El orquestador de saga ejecuta pasos secuencialmente. Si algún paso falla, ejecuta compensaciones en orden inverso para todos los pasos previamente completados. Esto asegura que el sistema retorne a un estado consistente, aunque las operaciones individuales ya fueran commiteadas.
Propiedades clave:
- Las compensaciones son operaciones de negocio en sí mismas, no comandos de base de datos
- Las compensaciones pueden fallar (ej. un reembolso rechazado por el procesador de pagos) y deben ser monitoreadas
- El log de saga registra lo que sucedió para auditoría e intervención manual
Lo que el código de producción añade sobre el orquestador de juguete de arriba: el log de saga vive en almacenamiento durable para que un orquestador que se cae pueda reanudar sagas en vuelo, y las compensaciones corren por un mecanismo de reintentos — una cola de compensación con backoff exponencial que escala a un humano cuando se agotan los reintentos. Un reembolso rechazado por el procesador de pagos no debería descartarse; debería despertar a alguien.
Variantes
| Variante | Coordinación | Caso de Uso |
|---|---|---|
| Saga Orquestada | Coordinador central maneja pasos y compensaciones | Workflows complejos con ordenamiento claro |
| Saga Coreografiada | Servicios emiten eventos; listeners disparan siguientes pasos o compensaciones | Arquitecturas desacopladas, orientadas a eventos |
| Saga Paralela | Pasos independientes corren concurrentemente; compensaciones corren para todos al fallar | Alto throughput, pasos débilmente acoplados |
| Saga Anidada | Un paso de saga es en sí mismo una sub-saga con sus propias compensaciones | Procesos de negocio recursivos |
Lo que funciona
- Diseña compensaciones desde el inicio. Son más difíciles de agregar retrospectivamente que las operaciones originales.
- Haz compensaciones idempotentes. Pueden ser reintentadas si el primer intento falla.
- Loggea todo. Estado de saga, resultados de compensación, y fallas deben ser observables.
- Establece timeouts. Un paso que cuelga para siempre bloquea toda la saga.
- Provee hooks para intervención manual. Algunas compensaciones requieren aprobación humana (ej. reembolsos sobre un umbral).
Errores Comunes
- Asumir que las compensaciones siempre tienen éxito. Los reembolsos de pago pueden ser rechazados; el inventario puede ya haber sido enviado.
- Faltar compensación para un paso. Cada paso de saga debe tener una contraparte definida.
- Compensaciones no idempotentes. Ejecutar una compensación dos veces no debería duplicar el reembolso.
- Perder estado de saga. Si el orquestador se cae, las sagas en vuelo deben ser recuperables desde un log persistente.
- Ignorar fallas parciales. Un paso que “medio tiene éxito” (ej. pago cobrado pero no registrado) es el caso más difícil de compensar.
Ejemplos del Mundo Real
Procesamiento de Órdenes E-Commerce
Realizar una orden involucra pago, reserva de inventario, y envío. Si el envío falla después de que el pago tiene éxito, la saga compensa reembolsando el pago y liberando el inventario.
Reserva de Viajes
Reservar un viaje involucra vuelos, hoteles y renta de autos. Si la reserva de hotel falla después de que el vuelo fue reservado, la saga cancela la reserva de vuelo (si es posible) y reembolsa al cliente.
Transferencias Bancarias
Una saga de transferencia inter-bancaria debita la cuenta origen, inicia un wire, y acredita la cuenta destino. Si el wire falla, la saga acredita de vuelta la cuenta origen (compensando el débito).
Lectura Adicional
- Sagas — Garcia-Molina & Salem, SIGMOD 1987 (paper original, PDF) — el paper que definió sagas y transacciones compensatorias.
- Patrón Saga — microservices.io — la entrada de Chris Richardson sobre coordinación de sagas.
- Patrón Compensating Transaction — Microsoft Learn — guía de arquitectura de Azure para deshacer trabajo distribuido.
- Repositorio companion ejecutable — el orquestador de saga de esta página, ejecutable en Python, Java y JavaScript.
Preguntas frecuentes
¿Cuál es la diferencia entre transacción compensatoria y rollback de base de datos?
El rollback deshace cambios no commiteados dentro de una única transacción de base de datos — las filas nunca existieron. La compensación deshace cambios ya commiteados, entre sistemas que no comparten un transaction manager, ejecutando contra-operaciones. El registro existe; escribes un registro que lo compensa.
¿Pueden compensarse todas las operaciones?
No. Algunas operaciones son irreversibles: un email ya enviado, un paquete ya despachado, una operación bursátil ya ejecutada. Para esas necesitas otra estrategia — una ventana de cancelación antes de commitear el paso, una corrección posterior (email de retractación, etiqueta de devolución), o aceptar la inconsistencia y alertar. Diseña la saga para que los pasos irreversibles corran lo más tarde posible.
¿Cómo se relaciona esto con el Patrón Saga?
Saga es la estrategia de coordinación — la orquestación o coreografía que secuencia transacciones locales. Compensating Transaction es el mecanismo de deshacer que Saga usa cuando un paso falla. Puedes tener sagas sin compensación completa (algunos equipos prefieren forward recovery), pero no puedes revertir una saga distribuida sin transacciones compensatorias.
¿Qué pasa cuando una compensación misma falla?
Lo mismo que cualquier otro fallo: reintenta con backoff, pero no la dejes morir en silencio. Los sistemas de producción mandan las compensaciones fallidas a una cola dedicada que reintenta hasta que tiene éxito o escala a intervención manual — una compensación atascada significa estado inconsistente, que es exactamente lo que el patrón existe para prevenir. Monitorea el log de compensaciones como monitoreas el flujo principal.
¿Las compensaciones deberían correr síncronas o como trabajos en cola?
Depende de qué tan rápido necesita respuesta el usuario. La compensación dentro del request es más simple y mantiene el demo legible, pero en producción normalmente se encolan — el orquestador marca la saga como "compensando" y un worker drena la cola con reintentos. Así una API de reembolsos lenta no mantiene el request original como rehén, y una caída transitoria del procesador no pierde el rollback.
¿En qué se diferencia de simplemente capturar una excepción y deshacer cosas?
En el alcance. Un try/catch que deshace funciona dentro de un proceso y una conexión. Una transacción compensatoria es una contra-operación de negocio durable que sobrevive reinicios del proceso, funciona entre servicios, y aparece en tu auditoría — "orden ORD-002 compensada: inventario liberado, pago reembolsado" es un evento de primera clase, no el efecto secundario de un manejador de errores.
Recursos Relacionados
Patrón Saga: orquestación vs coreografía
Gestiona transacciones distribuidas entre varios servicios encadenando transacciones locales con acciones compensatorias para el rollback.
PatternPatrón Outbox
Publica eventos de dominio de forma confiable persistiéndolos en una tabla outbox dentro de la misma transacción de base de datos que la operación de negocio.
PatternPatrón de Consumidor Idempotente
Procesa mensajes de una cola exactamente una vez sin importar los duplicados, usando operaciones idempotentes, identificadores únicos y estrategias de deduplicación.
RecipeGestionar Transacciones Distribuidas con el Saga Pattern
Cómo implementar orquestación y coreografía de sagas para mantener consistencia de datos entre microservicios sin transacciones distribuidas ni two-phase commit.
PatternPatrón Event-Carried State Transfer
Replica cambios de estado a través de servicios publicando eventos que llevan el estado completo actualizado de la entidad, permitiendo a los consumidores mantener sus propias copias locales sin consultar el origen.