El dolor: El monolito se rompió y las transacciones también

Cuando teníamos un monolito en Java, la vida era sencilla. Envolvías una lógica de negocio compleja en un método, le ponías una anotación @Transactional y la base de datos se encargaba de la magia ACID. Si algo fallaba, un rollback atómico nos salvaba. Pero migramos a microservicios y ese salvavidas desapareció.

El problema era clásico: un proceso de compra. Involucraba al servicio de Pedidos, al de Inventario y al de Pagos. ¿Qué pasaba si el pago se procesaba correctamente pero la actualización de inventario fallaba? Nos quedábamos con un cliente sin producto pero con un cargo en su tarjeta. Un desastre. El Two-Phase Commit (2PC) era una opción, pero el acoplamiento y los bloqueos que generaba iban en contra de la filosofía de microservicios. Necesitábamos otra cosa.

La solución: Entra el Patrón Saga

Un Saga no es más que una secuencia de transacciones locales. Cada servicio ejecuta su propia transacción atómica. Si un paso falla, el Saga ejecuta una serie de transacciones compensatorias para deshacer el trabajo ya hecho. Hay dos sabores principales:

  • Coreografía: Los servicios se comunican mediante eventos. El servicio de Pagos emite un PagoRealizado, Inventario lo escucha y actualiza el stock, luego emite InventarioActualizado, y así sucesivamente. Descentralizado, pero difícil de rastrear y depurar.
  • Orquestación: Un servicio central (el Orquestador) dirige el flujo. Le dice a Pagos que procese, luego a Inventario que actualice, etc. Si algo falla, el orquestador es responsable de invocar las compensaciones. Más control, un punto central de lógica.

Para nuestro flujo de compra, que tenía una lógica de negocio clara y necesitaba ser fácil de monitorear, elegimos la Orquestación.

Manos al código: Orquestando con Apache Camel y Quarkus

Decidimos usar Apache Camel dentro de nuestro stack de Quarkus para definir el Saga. Camel tiene un EIP (Enterprise Integration Pattern) para Sagas que es increíblemente poderoso y declarativo. Así es como se ve un orquestador de Saga simplificado para nuestro proceso de compra:


import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.saga.SagaPropagation;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class OrderSagaOrchestrator extends RouteBuilder {

    @Override
    public void configure() throws Exception {
        // Definimos el Saga
        from("direct:createOrderSaga")
            .saga(SagaPropagation.REQUIRES_NEW)
            // PASO 1: Procesar el pago
            .to("direct:processPayment")
            .compensation("direct:refundPayment") // Qué hacer si algo falla después de este paso
            
            // PASO 2: Actualizar el inventario
            .to("direct:updateInventory")
            .compensation("direct:restockInventory")
            
            // PASO 3: Completar la orden
            .to("direct:completeOrder")
            
            // Si todo sale bien, marcamos el Saga como completado
            .log("Saga de orden completado con éxito para el ID: ${header.orderId}");

        // --- Implementaciones de los pasos (endpoints) ---

        // Procesar pago (llamada a la API del servicio de pagos)
        from("direct:processPayment")
            .log("Procesando pago para la orden ${header.orderId}")
            // ... lógica de llamada a la API REST de Pagos
            .end();

        // Compensación: Reembolsar pago
        from("direct:refundPayment")
            .log("COMPENSACIÓN: Reembolsando pago para la orden ${header.orderId}")
            // ... lógica para llamar a la API de reembolso
            .end();

        // Actualizar inventario
        from("direct:updateInventory")
            .log("Actualizando inventario para la orden ${header.orderId}")
            // Simulación de un fallo para probar la compensación
            // .throwException(new RuntimeException("Error de inventario!"))
            .end();

        // Compensación: Reponer stock
        from("direct:restockInventory")
            .log("COMPENSACIÓN: Reponiendo inventario para la orden ${header.orderId}")
            // ... lógica para reponer el inventario
            .end();
            
        from("direct:completeOrder")
            .log("Completando orden ${header.orderId}")
            .end();
    }
}

Lecciones aprendidas desde la trinchera

Implementar Sagas no es trivial, pero el control que ganas vale la pena. Mis conclusiones clave:

  • La idempotencia es tu mejor amiga: Asegúrate de que tus operaciones y compensaciones puedan ejecutarse múltiples veces sin causar efectos secundarios. Si la red falla y el orquestador reintenta, no quieres duplicar un reembolso.
  • Observabilidad no es negociable: Necesitas un tracing distribuido (como OpenTelemetry) y dashboards claros para saber en qué estado está cada Saga. Sin esto, estás volando a ciegas.
  • Elige el modelo correcto: Para flujos simples y reactivos, la Coreografía puede funcionar bien. Para procesos de negocio complejos y con múltiples pasos como el nuestro, la Orquestación te da un control y una visibilidad que son oro puro.
  • Las compensaciones deben ser a prueba de fallos: Una transacción compensatoria NO debe fallar. Si tu lógica de reembolso falla, tienes un problema muy serio. Diseña estas operaciones para que sean lo más robustas y simples posible.

Al final, el patrón Saga nos permitió escalar nuestros servicios de forma independiente manteniendo la consistencia de los datos a través de todo el sistema. Fue un cambio de paradigma, pero uno que nos preparó para el futuro.