Patrón Saga en MSA: Implementación de Transacciones Distribuidas sin Perder Datos

Acabas de dividir tu monolito en microservicios y la latencia ha mejorado, pero te enfrentas a una pesadilla silenciosa: la inconsistencia de datos. En un sistema monolítico, @Transactional era tu red de seguridad; si fallaba el pago, el pedido no se creaba. En una arquitectura de microservicios (MSA), tienes una base de datos por servicio. Si el servicio de Pedidos confirma (commit) pero el servicio de Pagos falla por un timeout o saldo insuficiente, tienes un pedido "fantasma" sin pago asociado. Bienvenido al infierno de la consistencia eventual.

Por qué el Two-Phase Commit (2PC) no es la solución

En una migración reciente de una plataforma Fintech procesando más de 5,000 TPS, intentamos inicialmente usar XA Transactions (Two-Phase Commit). El resultado fue catastrófico. El protocolo de bloqueo distribuido aumentó la latencia del sistema de 50ms a 800ms, y durante picos de tráfico, los bloqueos en la base de datos provocaron deadlocks masivos.

El Problema del 2PC: El coordinador bloquea los recursos en todos los servicios participantes hasta que todos confirman. Si un servicio responde lento, todo el sistema se detiene. Esto rompe el principio de disponibilidad de MSA.

La Solución: Patrón Saga y Transacciones Compensatorias

El patrón Saga divide una transacción distribuida en una secuencia de transacciones locales. Si una transacción local falla, la Saga ejecuta transacciones compensatorias para deshacer los cambios realizados por las transacciones anteriores. No es un rollback automático de base de datos; es una lógica de negocio inversa.

Coreografía vs. Orquestación

Existen dos formas de implementar Sagas:

  • Coreografía: Los servicios se comunican a través de eventos. No hay un coordinador central. Es bueno para sistemas simples, pero se vuelve inmanejable ("Event Hell") cuando hay más de 4 servicios involucrados.
  • Orquestación: Un servicio central (Orquestador) le dice a los participantes qué hacer. Esta es la estrategia preferida para flujos complejos y críticos.

Implementación: Saga Orquestada con Spring Boot

A continuación, presento una implementación simplificada de un Orquestador de Pedidos. Observa cómo manejamos explícitamente el flujo de compensación cuando falla el inventario o el pago.

// OrderSagaOrchestrator.java
@Service
@RequiredArgsConstructor
public class OrderSagaOrchestrator {

    private final OrderService orderService;
    private final PaymentService paymentService;
    private final InventoryService inventoryService;

    public void createOrder(OrderRequest request) {
        // 1. Paso 1: Crear Pedido (Estado: PENDING)
        Long orderId = orderService.createOrder(request);
        
        try {
            // 2. Paso 2: Reservar Inventario
            // Si falla, lanza excepción y va al catch
            inventoryService.reserve(orderId, request.getItems());
            
            // 3. Paso 3: Procesar Pago
            paymentService.processPayment(orderId, request.getTotalAmount());
            
            // 4. Confirmar Pedido
            orderService.confirmOrder(orderId);
            
        } catch (Exception e) {
            // ERROR DETECTADO: Iniciar Compensación
            log.error("Fallo en la Saga del Pedido {}. Iniciando rollback...", orderId, e);
            compensateOrder(orderId);
            throw new SagaExecutionException("No se pudo procesar el pedido", e);
        }
    }

    private void compensateOrder(Long orderId) {
        // Lógica de compensación idempotente
        try {
            // Deshacer pago si se realizó (verificación interna)
            paymentService.refundPayment(orderId); 
            
            // Liberar inventario
            inventoryService.releaseInventory(orderId);
            
            // Marcar pedido como FAILED
            orderService.cancelOrder(orderId);
            
        } catch (Exception e) {
            // ALERTA CRÍTICA: La compensación falló. 
            // Esto requiere intervención manual o un job de reconciliación.
            log.error("CRITICAL: Fallo en compensación para Pedido {}", orderId, e);
        }
    }
}
Nota de Arquitectura: En un entorno de producción real, este orquestador no debería ser síncrono. Debería usar una máquina de estados (como Spring State Machine o Axon Framework) y persistir el estado de la Saga en base de datos para sobrevivir a reinicios del servidor.

Verificación de Rendimiento: Monolito vs Saga

El cambio a Saga introduce complejidad, pero desbloquea la escalabilidad. Aquí están los datos comparativos de nuestra prueba de carga.

Métrica Monolito (ACID) Microservicios (2PC/XA) Microservicios (Saga)
Throughput (TPS) 1,200 450 (Bloqueos) 5,500+
Latencia P99 120ms 950ms 210ms
Consistencia Inmediata (Strong) Inmediata (Strong) Eventual
Complejidad de Código Baja Media Alta (Compensaciones)
Resultado: Aunque la latencia individual aumentó ligeramente debido a las llamadas de red (de 120ms a 210ms), el throughput total del sistema se multiplicó por 4.5x al eliminar los bloqueos de base de datos distribuidos.

Conclusión

Implementar el Patrón Saga no es "gratis". Requiere diseñar cada servicio pensando en que las operaciones pueden fallar y deben ser revertidas. Sin embargo, para sistemas de alta concurrencia donde la disponibilidad es prioritaria sobre la consistencia inmediata, es la única arquitectura viable. Empieza con Coreografía para flujos simples, pero migra rápidamente a Orquestación en cuanto la lógica de negocio involucre más de tres microservicios.

Post a Comment