8 min read
Imaginá este escenario: tenés un e-commerce y un cliente acaba de hacer una orden. Tu sistema necesita ejecutar tres pasos en secuencia:
- Reservar el inventario en el servicio de bodega.
- Procesar el pago con la pasarela de pagos.
- Crear la guía de envío con el operador logístico.
En una aplicación monolítica con una sola base de datos, esto era sencillo. Abrías una transacción, ejecutabas los tres pasos, y si algo fallaba, hacías un rollback. Todo o nada. ACID (Atomicity, Consistency, Isolation, Durability — Atomicidad, Consistencia, Aislamiento y Durabilidad) al rescate.
Pero hoy vivimos en un mundo de microservicios. Cada uno de esos tres pasos lo maneja un servicio distinto, con su propia base de datos. No existe una transacción que los una a todos.
¿Qué pasa si el pago falla después de que ya reservaste el inventario? El stock queda bloqueado para siempre. ¿Qué pasa si la guía de envío falla después de cobrarle al cliente? Le cobraste pero nunca le llega nada.
Este es exactamente el problema que resuelve el Patrón Saga.
¿Qué es el Patrón Saga?
El Patrón Saga es una forma de manejar transacciones de larga duración en sistemas distribuidos. En lugar de una única transacción atómica, una Saga es una secuencia de transacciones locales, donde cada servicio ejecuta su parte y publica un evento o mensaje. Si algún paso falla, se ejecutan transacciones compensatorias para deshacer lo que ya se hizo.
Hay dos variantes principales para orquestar una Saga:
- Coreografía: cada servicio escucha eventos y decide qué hacer. No hay un coordinador central.
- Orquestación: un componente central (el orquestador) le dice a cada servicio qué hacer y en qué orden.
En este post vamos a enfocarnos en Orquestación, ya que es más fácil de entender, depurar y rastrear en producción.
Diagrama Patrón Saga
El flujo del caso de uso que describimos antes, implementado con el Patrón Saga, se vería así. La mitad superior muestra el flujo feliz: el orquestador dispara los tres pasos en secuencia y cada servicio responde con OK. La mitad inferior muestra el escenario de fallo: ShippingService falla, el orquestador detecta el problema y dispara las compensaciones en orden inverso.

Guía paso a paso
Paso 1 — Identificar las transacciones locales
Por cada servicio involucrado, definí cuál es la acción principal (forward transaction) y cuál es su reverso (compensating transaction):
| Servicio | Acción principal | Compensación |
|---|---|---|
| InventoryService | reserveStock() | releaseStock() |
| PaymentService | chargeCustomer() | refundCustomer() |
| ShippingService | createShipment() | cancelShipment() |
Paso 2 — Crear el Orquestador
El orquestador conoce el orden de los pasos y es el responsable de ejecutar las compensaciones si algo sale mal. No contiene lógica de negocio, solo coordina.
Paso 3 — Definir los pasos de la Saga
Cada paso encapsula la acción principal y su compensación. El orquestador los ejecuta en orden y, ante un fallo, recorre la lista en reversa ejecutando las compensaciones de los pasos que ya se completaron.
Paso 4 — Manejar la idempotencia
En sistemas distribuidos los mensajes pueden llegar más de una vez. Cada servicio debe poder recibir la misma instrucción múltiples veces sin efectos secundarios. Por ejemplo, si reserveStock() se llama dos veces para la misma orden, el resultado debe ser el mismo que si se llamó una sola vez.
Paso 5 — Observabilidad
Registrá el estado de la Saga en cada paso. Necesitás saber en qué punto está cada transacción para poder diagnosticar fallos y eventualmente recuperarse.
El código
Este ejemplo muestra cómo implementar el Patrón Saga con orquestación en Java. El caso de uso es la creación de una orden en un e-commerce.
import java.util.ArrayList;
import java.util.List;
// --- Interfaces ---
interface SagaStep {
void execute(OrderContext context) throws Exception;
void compensate(OrderContext context);
}
// --- Contexto de la Saga ---
class OrderContext {
private final String orderId;
private final String customerId;
private final String productId;
private final int quantity;
private final double amount;
private String reservationId;
private String paymentId;
private String shipmentId;
public OrderContext(String orderId, String customerId, String productId, int quantity, double amount) {
this.orderId = orderId;
this.customerId = customerId;
this.productId = productId;
this.quantity = quantity;
this.amount = amount;
}
// Gets y Sets
}
// --- Paso 1: Reservar Inventario ---
class ReserveInventoryStep implements SagaStep {
@Override
public void execute(OrderContext context) throws Exception {
System.out.println("[InventoryService] Reservando " + context.getQuantity()
+ " unidades del producto " + context.getProductId());
String reservationId = "RES-" + context.getOrderId();
context.setReservationId(reservationId);
System.out.println("[InventoryService] Reserva creada: " + reservationId);
}
@Override
public void compensate(OrderContext context) {
System.out.println("[InventoryService] Liberando reserva: " + context.getReservationId());
}
}
// --- Paso 2: Procesar Pago ---
class ProcessPaymentStep implements SagaStep {
private final boolean shouldFail;
public ProcessPaymentStep(boolean shouldFail) {
this.shouldFail = shouldFail;
}
@Override
public void execute(OrderContext context) throws Exception {
System.out.println("[PaymentService] Cobrando $" + context.getAmount()
+ " al cliente " + context.getCustomerId());
if (shouldFail) {
throw new Exception("Pago rechazado: fondos insuficientes");
}
String paymentId = "PAY-" + context.getOrderId();
context.setPaymentId(paymentId);
System.out.println("[PaymentService] Pago procesado: " + paymentId);
}
@Override
public void compensate(OrderContext context) {
System.out.println("[PaymentService] Reembolsando pago: " + context.getPaymentId());
}
}
// --- Paso 3: Crear Guía de Envío ---
class CreateShipmentStep implements SagaStep {
@Override
public void execute(OrderContext context) throws Exception {
System.out.println("[ShippingService] Creando guía de envío para orden: " + context.getOrderId());
String shipmentId = "SHIP-" + context.getOrderId();
context.setShipmentId(shipmentId);
System.out.println("[ShippingService] Guía creada: " + shipmentId);
}
@Override
public void compensate(OrderContext context) {
System.out.println("[ShippingService] Cancelando guía: " + context.getShipmentId());
}
}
// --- El Orquestador ---
class OrderSagaOrchestrator {
private final List<SagaStep> steps;
public OrderSagaOrchestrator(List<SagaStep> steps) {
this.steps = steps;
}
public void execute(OrderContext context) {
List<SagaStep> completedSteps = new ArrayList<>();
for (SagaStep step : steps) {
try {
step.execute(context);
completedSteps.add(step);
} catch (Exception e) {
System.out.println("\n[Orquestador] Fallo detectado: " + e.getMessage());
System.out.println("[Orquestador] Iniciando compensaciones...\n");
for (int i = completedSteps.size() - 1; i >= 0; i--) {
completedSteps.get(i).compensate(context);
}
System.out.println("\n[Orquestador] Saga finalizada con errores. Orden "
+ context.getOrderId() + " cancelada.");
return;
}
}
System.out.println("\n[Orquestador] Saga completada exitosamente. Orden "
+ context.getOrderId() + " confirmada.");
}
}
// --- Main ---
public class SagaPatternDemo {
public static void main(String[] args) {
System.out.println("=== ESCENARIO 1: Saga exitosa ===\n");
OrderContext context1 = new OrderContext("ORD-001", "CUST-42", "PROD-99", 3, 149.99);
List<SagaStep> steps1 = List.of(
new ReserveInventoryStep(),
new ProcessPaymentStep(false),
new CreateShipmentStep()
);
new OrderSagaOrchestrator(steps1).execute(context1);
System.out.println("\n=== ESCENARIO 2: Saga con fallo en el pago ===\n");
OrderContext context2 = new OrderContext("ORD-002", "CUST-43", "PROD-99", 3, 149.99);
List<SagaStep> steps2 = List.of(
new ReserveInventoryStep(),
new ProcessPaymentStep(true),
new CreateShipmentStep()
);
new OrderSagaOrchestrator(steps2).execute(context2);
}
}
Output esperado:
=== ESCENARIO 1: Saga exitosa ===
[InventoryService] Reservando 3 unidades del producto PROD-99
[InventoryService] Reserva creada: RES-ORD-001
[PaymentService] Cobrando $149.99 al cliente CUST-42
[PaymentService] Pago procesado: PAY-ORD-001
[ShippingService] Creando guía de envío para orden: ORD-001
[ShippingService] Guía creada: SHIP-ORD-001
[Orquestador] Saga completada exitosamente. Orden ORD-001 confirmada.
=== ESCENARIO 2: Saga con fallo en el pago ===
[InventoryService] Reservando 3 unidades del producto PROD-99
[InventoryService] Reserva creada: RES-ORD-002
[PaymentService] Cobrando $149.99 al cliente CUST-43
[Orquestador] Fallo detectado: Pago rechazado: fondos insuficientes
[Orquestador] Iniciando compensaciones...
[InventoryService] Liberando reserva: RES-ORD-002
[Orquestador] Saga finalizada con errores. Orden ORD-002 cancelada.
Para tener en cuenta en producción
El código anterior es un punto de partida claro, pero en un sistema real hay detalles adicionales que no podés ignorar:
- Persistencia del estado de la Saga: si tu orquestador se cae a la mitad, necesitás poder retomar desde donde quedó. Guardar el estado en base de datos es fundamental.
- Idempotencia en cada paso: los servicios deben tolerar llamadas duplicadas sin producir efectos dobles. Usar un
orderIdcomo clave de idempotencia es una práctica común. - Timeouts y reintentos: ¿qué pasa si un servicio tarda demasiado en responder? Definí umbrales claros y una estrategia de retry antes de escalar a compensación.
- Sagas asíncronas: para flujos más complejos o de mayor volumen, los pasos se ejecutan de forma asíncrona usando mensajería (Kafka, RabbitMQ). El orquestador reacciona a eventos en lugar de hacer llamadas directas.
Conclusión
El Patrón Saga no es complicado de entender, pero sí requiere disciplina para implementarlo bien. La clave está en pensar siempre en pares: por cada acción principal, definí su compensación. Si hacés eso desde el inicio, el resto fluye naturalmente.
La variante de orquestación que vimos aquí tiene una ventaja enorme en producción: el flujo completo está en un solo lugar, lo que facilita enormemente el diagnóstico de fallos y la trazabilidad.
En el próximo post vamos a explorar el Patrón Circuit Breaker, que complementa muy bien a las Sagas cuando necesitás proteger tus servicios de fallos en cascada.
Recordá suscribirte aquí para recibir los próximos posts directamente en tu correo.
Referencias:
Microservices Patterns — Chris Richardson
Building Microservices — Sam Newman