DEV Community

Christian Gonzales Komiya
Christian Gonzales Komiya

Posted on

[Spanish] ¿Cómo deshacer una operación en microservicios? El patrón Saga explicado con Java

Imagina que compras una laptop por internet. La tienda crea el pedido, reserva una unidad en el inventario e intenta cobrarte. Pero tu tarjeta está vencida.

El cobro falla. ¿Qué pasa con el pedido que ya se creó? ¿Y con la laptop que quedó reservada?

En una aplicación pequeña, quizá todas esas operaciones ocurran dentro de una misma transacción de base de datos. Si algo sale mal, un ROLLBACK puede deshacer los cambios.

En una arquitectura de microservicios, en cambio, cada servicio puede tener su propia base de datos y confirmar sus cambios de forma independiente. El ROLLBACK de un servicio no puede deshacer automáticamente lo que otro ya guardó.

Para resolver este tipo de situaciones existe un patrón conocido como Saga.

1. El problema: una operación de negocio, varios servicios

Supongamos que nuestra tienda tiene tres microservicios:

  • OrderService: registra y administra pedidos.
  • InventoryService: reserva y libera productos.
  • PaymentService: procesa los pagos.

El cliente ve una sola acción —comprar—, pero por detrás ocurren varias operaciones.

flowchart TD
    A["Cliente confirma la compra"] --> B["OrderService<br/>Crea pedido PENDING"]
    B --> C["InventoryService<br/>Reserva la laptop"]
    C --> D["PaymentService<br/>Intenta cobrar"]
    D -->|Pago aprobado| E["OrderService<br/>Confirma pedido"]
    D -->|Pago rechazado| F["Cancelar pedido"]
    F --> G["Liberar reserva"]

El camino feliz es sencillo: se reserva el producto, el pago se aprueba y el pedido queda confirmado.

El problema aparece cuando una operación falla después de que otras ya se completaron. Por ejemplo, si el pago es rechazado, no queremos dejar un pedido aparentemente válido ni mantener una unidad reservada indefinidamente.

2. Por qué no basta con hacer ROLLBACK

Una transacción ACID permite agrupar operaciones de base de datos para que se confirmen como una unidad o se reviertan. Pero ese alcance tiene un límite: normalmente es la transacción gestionada por una base de datos.

Si OrderService y InventoryService tienen bases de datos independientes, el ROLLBACK de una no deshace los cambios de la otra.

Paso Servicio Resultado
1 OrderService Pedido ORD-123 creado como PENDING
2 InventoryService Una laptop queda reservada
3 PaymentService El pago es rechazado
4 — Hay que cancelar el pedido y liberar la reserva

La clave es esta: no existe un ROLLBACK local que revierta automáticamente todas las operaciones ya confirmadas en servicios distintos.

3. La idea de Saga: compensar, no viajar al pasado

Una Saga convierte una operación de negocio en una secuencia de pasos locales. Cada paso confirma sus propios cambios. Si uno falla, se ejecutan acciones compensatorias para contrarrestar los pasos anteriores.

flowchart TD
    A["Crear pedido"] --> B["Reservar inventario"]
    B --> C["Procesar pago"]
    C -->|Éxito| D["Confirmar pedido"]
    C -->|Fallo| E["Compensar"]
    E --> F["Cancelar pedido"]
    F --> G["Liberar inventario"]

Una compensación no es necesariamente borrar datos. De hecho, conservar el historial suele ser importante para auditoría y soporte.

Operación original Posible compensación
Crear un pedido Cancelar el pedido
Reservar una unidad Liberar la reserva
Cobrar al cliente Reembolsar el importe
Reservar una habitación Liberar la habitación

La compensación debe reflejar las reglas del negocio. Por ejemplo, si el pago se rechazó antes de cobrar, no corresponde hacer un reembolso; basta con cancelar el pedido y liberar la reserva.

4. Un ejemplo con Java y Spring Boot

Para entender el mecanismo, imaginemos un servicio que coordina los pasos del checkout. Los clientes HTTP que aparecen en el ejemplo representan llamadas a los otros microservicios.

El código es ilustrativo: se centra en el flujo de la Saga, no en toda la configuración de Spring Boot.

@Service
public class CheckoutService {

    private final OrderClient orders;
    private final InventoryClient inventory;
    private final PaymentClient payments;

    public CheckoutService(
            OrderClient orders,
            InventoryClient inventory,
            PaymentClient payments) {
        this.orders = orders;
        this.inventory = inventory;
        this.payments = payments;
    }

    public void checkout(String orderId) {
        // El pedido ya existe con estado PENDING.
        inventory.reserve(orderId);

        try {
            payments.charge(orderId);
            orders.confirm(orderId);

        } catch (PaymentRejectedException ex) {
            // Acciones compensatorias.
            orders.cancel(orderId);
            inventory.release(orderId);

            throw ex;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

El flujo se puede leer así:

  1. El pedido ya fue creado con estado PENDING.
  2. Se reserva el producto.
  3. Se intenta procesar el pago.
  4. Si el pago se aprueba, se confirma el pedido.
  5. Si el pago se rechaza, se cancela el pedido y se libera la reserva.

En un sistema real, OrderClient, InventoryClient y PaymentClient podrían implementarse con WebClient, HTTP Interfaces de Spring o un cliente de mensajería.

¿Cómo sería la compensación en OrderService?

Un endpoint puede cambiar el estado del pedido sin borrar el registro:

@PostMapping("/{orderId}/cancel")
public void cancel(@PathVariable String orderId) {
    Order order = repository.findById(orderId)
        .orElseThrow();

    if (order.getStatus() == OrderStatus.CANCELLED) {
        return; // Repetir la petición no vuelve a cancelar.
    }

    if (order.getStatus() != OrderStatus.PENDING) {
        throw new IllegalStateException(
            "El pedido no se puede cancelar"
        );
    }

    order.setStatus(OrderStatus.CANCELLED);
    repository.save(order);
}
Enter fullscreen mode Exit fullscreen mode

Este ejemplo incorpora una comprobación de idempotencia básica: si el pedido ya está cancelado, repetir la petición no vuelve a modificarlo. En producción, las reglas de concurrencia y las transiciones de estado deben protegerse también a nivel de persistencia.

5. ¿Qué pasa si falla la compensación?

Aquí es donde el diseño se vuelve interesante.

Supongamos que el pago es rechazado, pero InventoryService está temporalmente fuera de servicio. El pedido puede cancelarse, pero la reserva sigue activa.

No conviene confiar en que una llamada HTTP tendrá éxito siempre. Una implementación robusta debe contemplar, al menos:

  • Reintentos controlados: volver a intentar la compensación ante errores transitorios.
  • Idempotencia: repetir una solicitud no debe liberar dos veces el mismo stock ni duplicar un reembolso.
  • Persistencia del progreso: registrar qué pasos se completaron y qué compensaciones siguen pendientes.
  • Observabilidad: contar con logs, métricas y alertas para detectar Sagas atascadas.
  • Timeouts y recuperación: decidir qué hacer si un servicio no responde o el resultado de una llamada es incierto.

También hay una diferencia importante entre un rechazo de negocio y un error técnico. Una tarjeta vencida es un rechazo claro. Un timeout, en cambio, no demuestra que el cobro haya fallado: el proveedor podría haber cobrado y haberse perdido la respuesta. Antes de reembolsar o repetir el cobro, hay que consultar el estado de la operación o utilizar una clave de idempotencia.

6. Orquestación y coreografía

Hay dos formas habituales de coordinar una Saga.

Orquestación

Un componente coordinador decide qué paso ejecutar y qué compensaciones iniciar.

flowchart TD
    O["Checkout Orchestrator"] --> A["OrderService"]
    O --> B["InventoryService"]
    O --> C["PaymentService"]

La ventaja es que el flujo se puede leer en un lugar. El reto es que el orquestador necesita guardar su progreso y recuperarse después de un fallo.

Coreografía

Cada servicio publica eventos y otros servicios reaccionan a ellos. Por ejemplo, OrderCreated puede iniciar la reserva; PaymentRejected puede provocar la cancelación del pedido y la liberación del inventario.

flowchart LR
    A["OrderService"] -->|"OrderCreated"| B["InventoryService"]
    B -->|"InventoryReserved"| C["PaymentService"]
    C -->|"PaymentRejected"| D["OrderService"]
    D -->|"OrderCancelled"| B

La coreografía evita concentrar toda la coordinación en un único componente, pero puede ser más difícil seguir el flujo completo cuando hay muchos servicios y eventos.

Ningún enfoque elimina la necesidad de gestionar fallos, duplicados y reintentos. La elección depende de la complejidad del proceso y de cómo el equipo quiera mantenerlo.

7. Un detalle práctico: guardar el cambio y publicar el evento

En una Saga basada en eventos aparece otro posible fallo: el servicio guarda un cambio en su base de datos, pero se cae antes de publicar el evento que informa a los demás.

El patrón Transactional Outbox ayuda a resolverlo. El servicio guarda el cambio de negocio y el evento pendiente en la misma transacción local. Otro proceso publica los eventos pendientes y los marca como enviados.

flowchart TD
    A["Una transacción local"] --> B["Actualizar pedido"]
    A --> C["Guardar evento en Outbox"]
    C --> D["Publicador de eventos"]
    D --> E["Broker de mensajes"]
    E --> F["Servicios consumidores"]

El publicador puede enviar un evento más de una vez, así que los consumidores deben ser idempotentes. Outbox ayuda a no perder la publicación entre la escritura local y el envío; no convierte toda la Saga en una transacción ACID global.

8. ¿Cuándo tiene sentido usar Saga?

Saga resulta útil cuando una operación de negocio atraviesa varios servicios independientes y no es viable —o no conviene— compartir una transacción de base de datos entre todos ellos.

No hace falta aplicar el patrón a cada llamada entre microservicios. Si una operación solo consulta información o si un servicio puede completar su trabajo de forma independiente, quizá no necesite una Saga.

Antes de implementarla, conviene responder estas preguntas:

  1. ¿Cuáles son los pasos del proceso?
  2. ¿Qué puede fallar en cada paso?
  3. ¿Qué acción compensa cada paso que ya se completó?
  4. ¿Cómo se reintenta una compensación sin duplicar efectos?
  5. ¿Cómo se recupera el proceso tras reiniciar un servicio?

Conclusión

En un sistema distribuido, fallar a mitad de una operación es algo que hay que diseñar, no una excepción que se pueda ignorar.

El patrón Saga organiza los pasos de un proceso y define cómo compensar los que ya se completaron si algo sale mal. A cambio, exige pensar en estados intermedios, reintentos, idempotencia y recuperación.

La idea no es que los microservicios carezcan de transacciones. Cada servicio puede usar transacciones ACID en su propia base de datos. Lo que no se obtiene automáticamente es una única transacción ACID que abarque todos los servicios independientes.

La pregunta útil no es solo «¿qué pasa si todo sale bien?», sino también «¿cómo dejamos el negocio en un estado coherente si falla el paso número tres?».

Top comments (0)