El Saga Pattern es un patrón de diseño para gestionar transacciones distribuidas en arquitecturas de microservicios. En lugar de usar transacciones ACID tradicionales (que no funcionan bien entre servicios), una Saga coordina una serie de transacciones locales mediante eventos.
En microservicios con bases de datos independientes:
- ❌ No puedes usar transacciones distribuidas (2PC)
- ❌ No puedes hacer rollback automático entre servicios
- ❌ Necesitas mantener consistencia eventual
Solución: Una secuencia de transacciones locales donde cada paso:
- Completa su transacción local
- Publica un evento
- Trigger del siguiente paso
Si algo falla, ejecutar transacciones compensatorias para deshacer cambios.
Cada servicio escucha eventos y decide qué hacer. No hay coordinador central.
Ventajas:
- Simple para flujos cortos
- Bajo acoplamiento
- Sin single point of failure
Desventajas:
- Difícil de entender el flujo completo
- Complejidad crece con más pasos
- Difícil de debuggear
Cuándo usar:
- Flujos simples (2-3 pasos)
- Alta autonomía de servicios
- Baja interdependencia
Un coordinador central (Saga Orchestrator) controla el flujo.
Ventajas:
- Flujo explícito y fácil de entender
- Fácil de debuggear
- Control centralizado de compensación
Desventajas:
- Potencial single point of failure
- Más acoplamiento al orquestador
Cuándo usar:
- Flujos complejos (4+ pasos)
- Necesitas visibilidad del estado
- Lógica de compensación compleja
Usamos Choreography para la creación de órdenes porque es un flujo relativamente simple y queremos mantener servicios desacoplados.
1. Cliente → POST /api/orders
└─> Orders API crea orden
└─> Publica: OrderCreatedEvent
2. Inventory API escucha OrderCreatedEvent
└─> Reserva stock
└─> Publica: InventoryReservedEvent
3. Payments API escucha OrderCreatedEvent
└─> Espera InventoryReservedEvent
└─> Procesa pago
└─> Publica: PaymentApprovedEvent
4. Orders API escucha PaymentApprovedEvent
└─> Confirma orden
└─> Publica: OrderConfirmedEvent
5. Notifications API escucha OrderConfirmedEvent
└─> Envía confirmación al cliente
Escenario: Payment Falla
1-2. [Igual que happy path]
3. Payments API procesa pago
└─> Payment Gateway rechaza
└─> Publica: PaymentRejectedEvent
4. Inventory API escucha PaymentRejectedEvent
└─> Libera reserva (COMPENSACIÓN)
└─> Publica: InventoryReleasedEvent
5. Orders API escucha PaymentRejectedEvent
└─> Cancela orden (COMPENSACIÓN)
└─> Publica: OrderCancelledEvent
6. Notifications API escucha OrderCancelledEvent
└─> Notifica al cliente
Escenario: No Hay Stock
1. Cliente → POST /api/orders
└─> Orders API crea orden
└─> Publica: OrderCreatedEvent
2. Inventory API escucha OrderCreatedEvent
└─> Verifica stock → NO HAY SUFICIENTE
└─> Publica: InventoryReservationFailedEvent
3. Payments API escucha InventoryReservationFailedEvent
└─> NO procesa pago (compensación preventiva)
4. Orders API escucha InventoryReservationFailedEvent
└─> Cancela orden (COMPENSACIÓN)
└─> Publica: OrderCancelledEvent
5. Notifications API escucha OrderCancelledEvent
└─> Notifica al cliente (sin stock)
NO CÓDIGO REAL - Solo concepto:
// State Machine para tracking
public enum OrderSagaState
{
Started,
InventoryReserved,
PaymentProcessed,
OrderConfirmed,
Completed,
Failed,
Compensating,
Compensated
}
// Saga State Storage
public class OrderSagaState
{
public Guid OrderId { get; set; }
public OrderSagaState State { get; set; }
public DateTime StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public List<SagaStep> CompletedSteps { get; set; }
public string FailureReason { get; set; }
}
// Cada paso de la saga
public class SagaStep
{
public string Name { get; set; }
public DateTime ExecutedAt { get; set; }
public string EventId { get; set; }
public bool IsCompensated { get; set; }
}NO CÓDIGO REAL - Solo concepto:
// En Orders API
public class PaymentApprovedEventHandler : IEventHandler<PaymentApprovedEvent>
{
public async Task HandleAsync(PaymentApprovedEvent @event)
{
// 1. Idempotencia
if (await IsAlreadyProcessed(@event.EventId))
return;
// 2. Obtener orden
var order = await _orderRepository.GetByIdAsync(@event.OrderId);
// 3. Validar estado (puede estar cancelada por timeout)
if (order.Status == OrderStatus.Cancelled)
{
_logger.Warning("Order already cancelled, ignoring payment approval");
return;
}
// 4. Cambiar estado
order.Confirm();
await _orderRepository.UpdateAsync(order);
// 5. Publicar evento
await _eventBus.PublishAsync(new OrderConfirmedEvent
{
OrderId = order.Id,
ConfirmedAt = DateTime.UtcNow
});
// 6. Marcar como procesado
await MarkAsProcessed(@event.EventId);
}
}
// Compensación en Inventory API
public class PaymentRejectedEventHandler : IEventHandler<PaymentRejectedEvent>
{
public async Task HandleAsync(PaymentRejectedEvent @event)
{
// 1. Idempotencia
if (await IsAlreadyProcessed(@event.EventId))
return;
// 2. Buscar reserva
var reservation = await _reservationRepository
.GetByOrderIdAsync(@event.OrderId);
if (reservation == null)
{
_logger.Warning("No reservation found for order {OrderId}", @event.OrderId);
return;
}
// 3. Liberar stock (COMPENSACIÓN)
await _stockService.ReleaseReservationAsync(reservation.Id);
// 4. Publicar evento
await _eventBus.PublishAsync(new InventoryReleasedEvent
{
OrderId = @event.OrderId,
ReservationId = reservation.Id,
Reason = "Payment rejected"
});
// 5. Marcar como procesado
await MarkAsProcessed(@event.EventId);
}
}CRÍTICO: Todos los handlers deben ser idempotentes.
// NO CÓDIGO REAL
// Usar tabla de eventos procesados
public async Task<bool> IsAlreadyProcessed(string eventId)
{
return await _db.ProcessedEvents
.AnyAsync(e => e.EventId == eventId);
}
public async Task MarkAsProcessed(string eventId)
{
await _db.ProcessedEvents.AddAsync(new ProcessedEvent
{
EventId = eventId,
ProcessedAt = DateTime.UtcNow
});
await _db.SaveChangesAsync();
}Configurar timeouts para cada paso:
- Inventory Reservation: 5 minutos
- Payment Processing: 2 minutos
- Overall Saga: 10 minutos
Si expira, ejecutar compensación automática.
Almacenar estado de la saga para:
- Monitoring
- Debugging
- Recovery
- Reanudar sagas interrumpidas
OrderSagaState table:
- OrderId
- CurrentState
- StartedAt
- LastUpdatedAt
- CompletedSteps (JSON)
- FailureReason
Cada acción debe tener su compensación:
| Acción | Compensación |
|---|---|
| Create Order | Cancel Order |
| Reserve Inventory | Release Reservation |
| Process Payment | Refund Payment |
| Send Notification | Send Cancellation Notice |
Compensar en orden inverso a la ejecución:
Ejecución:
1. Create Order
2. Reserve Inventory
3. Process Payment
Compensación:
3. Refund Payment
2. Release Inventory
1. Cancel Order
Usar locks semánticos en lugar de locks de base de datos:
// NO CÓDIGO REAL
public class Order
{
public OrderStatus Status { get; private set; }
// Semantic lock via estado
public bool CanBeModified()
{
return Status == OrderStatus.Pending ||
Status == OrderStatus.Created;
}
public void Confirm()
{
if (!CanBeModified())
throw new InvalidOperationException("Cannot confirm order in current state");
Status = OrderStatus.Confirmed;
}
}Propagar Correlation ID en todos los eventos:
Event 1: OrderCreatedEvent
└─> CorrelationId: abc-123
Event 2: InventoryReservedEvent
└─> CorrelationId: abc-123
└─> CausationId: [EventId de OrderCreatedEvent]
Event 3: PaymentApprovedEvent
└─> CorrelationId: abc-123
└─> CausationId: [EventId de InventoryReservedEvent]
Monitorear:
- Sagas en progreso
- Sagas completadas (tasa de éxito)
- Sagas fallidas
- Tiempo promedio de completación
- Paso donde más fallan
Configurar alertas para:
- Saga excede timeout
- Tasa de fallos > 5%
- Compensaciones frecuentes
- Eventos duplicados
Testear cada handler individualmente:
// NO CÓDIGO REAL
[Fact]
public async Task PaymentApprovedEventHandler_Should_ConfirmOrder()
{
// Arrange
var handler = new PaymentApprovedEventHandler(...);
var @event = new PaymentApprovedEvent { OrderId = orderId };
// Act
await handler.HandleAsync(@event);
// Assert
var order = await _repository.GetByIdAsync(orderId);
Assert.Equal(OrderStatus.Confirmed, order.Status);
}Testear flujo completo de saga:
// NO CÓDIGO REAL
[Fact]
public async Task OrderCreationSaga_HappyPath_Should_CompleteSuccessfully()
{
// Arrange
var orderRequest = CreateOrderRequest();
// Act
var orderId = await CreateOrder(orderRequest);
// Assert - Verificar cada paso
await WaitFor(() => InventoryIsReserved(orderId));
await WaitFor(() => PaymentIsProcessed(orderId));
await WaitFor(() => OrderIsConfirmed(orderId));
await WaitFor(() => NotificationIsSent(orderId));
}
[Fact]
public async Task OrderCreationSaga_PaymentFails_Should_Compensate()
{
// Arrange - Configurar payment gateway para fallar
ConfigurePaymentToFail();
// Act
var orderId = await CreateOrder(orderRequest);
// Assert - Verificar compensación
await WaitFor(() => InventoryIsReleased(orderId));
await WaitFor(() => OrderIsCancelled(orderId));
}Síntoma: Saga no progresa
Diagnóstico:
- Verificar logs del último paso completado
- Revisar dead letter queue
- Verificar que todos los servicios estén up
Solución:
- Re-publicar evento manualmente si se perdió
- Ejecutar compensación manual si es necesario
- Implementar timeout automation
Síntoma: Algunos pasos compensados, otros no
Diagnóstico:
- Revisar estado de saga
- Ver qué servicios fallaron en compensación
Solución:
- Manual intervention para completar compensación
- Implementar retry automático para compensaciones