Patrones para Sistemas Distribuidos con Spring Boot: Guía PRO (Parte 2)

Cesar A. Fernandez B.

Cesar A. Fernandez B.

Sr. Software Engineer

23 de abril de 2025 11 min
Artículo

4. El Patrón API Gateway: Tu guardián PRO!!! de tus Microservicios

Cuando tienes un montón de microservicios, no quieres que tus clientes (la app web, la app móvil) tengan que saber la dirección de cada uno y lidiar con todos ellos. Qué va!, no cuadra, para eso está el API Gateway. Es un único punto de entrada de tus servicios.

Qué hace este "portero"?

  • Enrutamiento (Routing): Recibe la petición del cliente (ej: /api/v1/ordenes) y la redirige al microservicio correcto (ServicioOrdenes).

  • Agregación: A veces, una pantalla en tu app necesita datos de varios servicios. El Gateway puede hacer esas llamadas internas y devolver una respuesta combinada.

  • Offloading de Responsabilidades Comunes: Tareas como:

    • Autenticación y Autorización: Verifica quién es el usuario y qué puede hacer, antes de dejarlo pasar a los servicios internos.
    • Rate Limiting: Evita que un cliente abuse y haga demasiadas peticiones.
    • Logging y Monitoreo: Centraliza el registro de quién llama a qué.
    • Transformación de Protocolos: Quizás tus servicios internos hablan REST pero un cliente viejo necesita SOAP (ojalá no, es una lata :S pero pasa!!!).
    • Cacheo: Guardar respuestas comunes para no pegarle tanto a los servicios.

Spring Cloud Gateway al Mando:

Es la opción moderna y reactiva (basada en Project Reactor) de Spring para montar un API Gateway. Es súper potente y configurable.

Vamos con el ejemplito: Ruteando el E-commerce

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApiGatewayConfiguration {

    @Bean
    public RouteLocator gatewayRouter(RouteLocatorBuilder builder) {
        return builder.routes()
                // Ruta para el Servicio de Órdenes
                .route("servicio_ordenes", r -> r.path("/api/v1/ordenes/**") // Si la petición empieza con esto...
                        // Agrega un header antes de reenviar (ejemplo de filtro)
                        .filters(f -> f.addRequestHeader("X-Gateway-Source", "WebClient"))
                        // ...la manda al servicio registrado en Eureka/Consul como "SERVICIO-ORDENES"
                        // "lb://" significa Load Balanced: usará el discovery service. PRO!!!
                        .uri("lb://SERVICIO-ORDENES"))

                // Ruta para el Servicio de Pagos
                .route("servicio_pagos", r -> r.path("/api/v1/pagos/**")
                        // Podrías añadir rate limiting aquí con otro filtro
                        // .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(myRateLimiter())))
                        .uri("lb://SERVICIO-PAGOS"))

                // Ruta para el Servicio de Usuarios (quizás con otro path base)
                .route("servicio_usuarios", r -> r.path("/auth/**", "/api/v1/usuarios/**")
                         .uri("lb://SERVICIO-USUARIOS"))

                .build();
    }
}

// Necesitarás dependencias de Spring Cloud Gateway y un cliente de Discovery (Eureka, Consul)
// Y configurar el nombre de tus servicios (spring.application.name) en cada microservicio
// y la ubicación del discovery server en application.properties/yml del gateway y los servicios.

PRO Tip: No metas lógica de negocio en el Gateway. Su trabajo es enrutar y manejar tareas transversales (seguridad, tráfico), no calcular precios ni validar inventario. Mantenlo ligero y enfocado. Además, asegúrate de que tu Gateway sea altamente disponible, porque si se cae él, nadie entra a tus servicios!!!

Diagrama del Patrón API Gateway


5. El Patrón Saga: Transacciones distribuidas sin sustos

Muchachones aquí la cosa se pone seria. Tienes microservicios, cada uno con su propia base de datos. Cómo manejas una operación que involucra a varios de ellos y tiene que ser "todo o nada"? Por ejemplo, crear una orden: 1) Reserva pago (Servicio Pagos), 2) Descuenta inventario (Servicio Inventario), 3) Programa envío (Servicio Envíos). Si el inventario falla, tienes que deshacer el pago reservado. Una verdadera lata!!!

Las transacciones distribuidas tradicionales (2PC) son complejas y acoplan mucho los servicios. Saga es un enfoque diferente para mantener la consistencia. Saga es una secuencia de transacciones locales en cada servicio, lo que permite tomar medidas compensatorias, es decir, si un paso falla, Saga ejecuta transacciones para deshacer lo que ya se hizo en los pasos anteriores.

Aqui tenemos 2 sabores principales:

  1. Coreografía (Choreography): Más basado en eventos. Cada servicio, al terminar su parte, publica un evento. El siguiente servicio en la secuencia escucha ese evento y hace su parte. Si algo falla, publica un evento de "fallo", y los servicios anteriores escuchan y ejecutan su compensación. Es más desacoplado pero puede ser difícil seguirle la pista al flujo completo.

  2. Orquestación (Orchestration): Hay un "director de orquesta" central (el orquestador de Saga) que le dice a cada servicio qué hacer. El orquestador llama al Servicio A, espera respuesta, llama al Servicio B, etc. Si algo falla, el orquestador es responsable de llamar a las operaciones compensatorias en orden inverso. Es más fácil de entender y monitorear, pero introduce un punto central.

Spring Boot y Sagas:

  • Axon Framework: Tiene soporte de primera para Sagas (ambos estilos).

  • Spring Cloud Stream / Kafka / RabbitMQ: Puedes implementar Coreografía usando eventos.

  • Frameworks de Orquestación (ej: Camunda, AWS Step Functions): Herramientas más especializadas que se pueden integrar. O puedes codificar tu propio orquestador simple.

Veamos un ejemplo Conceptual: Saga de Creación de Orden (Orquestada)

@Service // Este sería el Orquestador
public class OrderCreationSagaOrchestrator {

    // Inyectar clientes para hablar con los otros servicios (Feign, RestTemplate, WebClient)
    private final PaymentServiceClient paymentClient;
    private final InventoryServiceClient inventoryClient;
    private final ShippingServiceClient shippingClient;
    // Podrías necesitar un repositorio para guardar el estado de la Saga

    public void iniciarSagaCreacionOrden(Order order) {
        System.out.println("Iniciando Saga para Orden: " + order.getId());
        boolean pagoOk = false;
        boolean inventarioOk = false;
        // Podrías guardar el estado inicial de la saga aquí

        try {
            // Paso 1: Reservar Pago
            System.out.println("Saga - Paso 1: Reservando pago...");
            paymentClient.reservarPago(order.getPaymentDetails());
            pagoOk = true;
            System.out.println("Saga - Paso 1: Pago reservado OK.");
            // Actualizar estado de la saga

            // Paso 2: Reservar Inventario
            System.out.println("Saga - Paso 2: Reservando inventario...");
            inventoryClient.reservarItems(order.getItems());
            inventarioOk = true;
            System.out.println("Saga - Paso 2: Inventario reservado OK.");
            // Actualizar estado de la saga

            // Paso 3: Programar Envío
            System.out.println("Saga - Paso 3: Programando envío...");
            shippingClient.programarEnvio(order);
            System.out.println("Saga - Paso 3: Envío programado OK.");
            // Marcar la saga como completada

            System.out.println("¡Saga completada con éxito para Orden: " + order.getId() + "!");

        } catch (Exception e) {
            System.err.println("¡FALLO en la Saga para Orden " + order.getId() + "! Iniciando compensación...");
            // Marcar la saga como fallida

            // Iniciar compensaciones EN ORDEN INVERSO
            if (inventarioOk) {
                try {
                    System.out.println("Saga - Compensación: Liberando inventario...");
                    inventoryClient.liberarItemsReservados(order.getItems()); // Operación compensatoria
                    System.out.println("Saga - Compensación: Inventario liberado.");
                } catch (Exception compEx) {
                    // Houston, tenemos un problema SERIO!!! La compensación falló. Requiere intervención manual.
                    System.err.println("¡FALLO CRÍTICO al compensar inventario! " + compEx.getMessage());
                    // Loggear TODO, alertar...
                }
            }
            if (pagoOk) {
                try {
                    System.out.println("Saga - Compensación: Cancelando reserva de pago...");
                    paymentClient.cancelarReservaPago(order.getPaymentDetails()); // Operación compensatoria
                    System.out.println("Saga - Compensación: Reserva de pago cancelada.");
                } catch (Exception compEx) {
                     // Otro fallo crítico!!! Mier...
                    System.err.println("¡FALLO CRÍTICO al compensar pago! " + compEx.getMessage());
                }
            }
            // Lanzar o manejar la excepción original del fallo de la saga
            throw new SagaExecutionException("La saga falló durante la ejecución.", e);
        }
    }
}

// Necesitarías los clientes (Feign interfaces, etc.) y las operaciones compensatorias en cada servicio.
// interface PaymentServiceClient { void reservarPago(...); void cancelarReservaPago(...); }
// interface InventoryServiceClient { void reservarItems(...); void liberarItemsReservados(...); }
// interface ShippingServiceClient { void programarEnvio(...); /* Quizás cancelar envío si aplica */ }
// class SagaExecutionException extends RuntimeException { ... }

PRO Tip: Las Sagas son complejas. Mucho!!! Diseñar las transacciones locales y, sobre todo, las compensatorias requiere cuidado. Una compensación debe ser idempotente (si la llamas varias veces, el resultado es el mismo) e, idealmente, no debería fallar (aunque tienes que planificar qué hacer si falla). La coreografía es más "pura" en términos de desacoplamiento, pero la orquestación suele ser más fácil de depurar y entender al principio. Elige el sabor que mejor se adapte a tu equipo y complejidad. (Insisto aqui si hay que pensarlo bien y hacer muchos diagramitas, no se lo tomen a la ligera, aca chatgpt no los salvara jaja jaja)

Diagrama del Patrón Saga


6. El Patrón Circuit Breaker: El Fusible Inteligente que te Salva la Patria!!!

En un sistema distribuido, un servicio llama a otro. Pero, qué pasa si ese otro servicio está lento, caído, o respondiendo con errores todo el tiempo? Si tu servicio sigue intentando llamarlo sin parar, puede:

  1. Agotar sus propios recursos (hilos, conexiones) esperando respuestas que nunca llegan.

  2. Empeorar la situación del servicio caído bombardeándolo con más peticiones.

  3. Crear un efecto dominó (cascading failure) donde el fallo de un servicio tumba a otros que dependen de él. Para quedar calvo pes!!!

El Circuit Breaker actúa como un fusible eléctrico inteligente entre el que llama (tu servicio) y el que es llamado (la dependencia externa o interna).

Cómo funciona mi bro?

  1. Cerrado (Closed): Al principio, el circuito está cerrado, las llamadas pasan normal. El breaker monitorea los fallos.

  2. Abierto (Open): Si el número de fallos supera un umbral (ej: 50% de errores en las últimas 10 llamadas), el circuito se "abre". De una!!! Durante un tiempo configurado (ej: 30 segundos), todas las llamadas a ese servicio fallan inmediatamente sin siquiera intentar contactarlo. Esto le da un respiro al servicio caído y protege al que llama. Usualmente, se devuelve un error rápido o se ejecuta una lógica de fallback.

  3. Semi-Abierto (Half-Open): Después del tiempo de espera en "abierto", el circuito entra en estado semi-abierto. Permite que unas pocas llamadas de prueba pasen. Si estas llamadas tienen éxito, el circuito se cierra de nuevo (el servicio parece haberse recuperado!!!). Si fallan, vuelve a abrirse y el ciclo se repite.

Spring Cloud Circuit Breaker con Resilience4j:

Antes se usaba Hystrix (de Netflix), pero ahora Resilience4j es la opción moderna y recomendada en el ecosistema Spring. Es súper fácil de integrar.

Como siempre vamos con el ejemplito: Protegiendo una llamada a un servicio externo

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate; // O WebClient para reactivo

@Service
public class ExternalServiceClient {

    private final RestTemplate restTemplate;

    public ExternalServiceClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    // Aquí aplicamos el fusible!!!
    @CircuitBreaker(name = "servicioExternoCritico", fallbackMethod = "llamarServicioExternoFallback")
    // 'name' se usa para la configuración en application.properties
    // 'fallbackMethod' es el método a llamar si el circuito está abierto o la llamada falla.
    public String llamarServicioExterno(String parametro) {
        System.out.println("Intentando llamar al servicio externo...");
        // Simula la llamada que puede fallar
        String url = "http://servicio-externo-lento.com/api/data?p=" + parametro;
        // restTemplate.getForObject(...) puede lanzar RestClientException si falla
        String resultado = restTemplate.getForObject(url, String.class);
        System.out.println("Llamada externa exitosa!!!");
        return "Respuesta real: " + resultado;
    }

    // El método Fallback! IMPORTANTE: Debe tener la misma firma que el método original,
    // Mas un parámetro extra al final para recibir la excepción que causó el fallo.
    public String llamarServicioExternoFallback(String parametro, Throwable ex) {
        System.err.println("¡Circuito Abierto o Fallo al llamar servicio externo! Parámetro: " + parametro + ", Error: " + ex.getMessage());
        // Lógica de contingencia: devolver un valor por defecto, datos de caché, un mensaje amigable...
        // NUNCA!!! intentes llamar al mismo servicio aquí adentro! Sería un bucle infinito.
        return "Respuesta de Fallback: No se pudo obtener la data externa en este momento.";
    }

    // --- Configuración en application.properties o application.yml ---
    /*
    # Configuración básica para el circuit breaker llamado 'servicioExternoCritico'
    resilience4j.circuitbreaker:
      instances:
        servicioExternoCritico:
          registerHealthIndicator: true        # Para que aparezca en el health endpoint de Spring Boot Actuator
          slidingWindowType: COUNT_BASED       # Basado en las últimas N llamadas (o TIME_BASED)
          slidingWindowSize: 20                # Considerar las últimas 20 llamadas
          failureRateThreshold: 50             # Si 50% o más fallan, abrir circuito
          minimumNumberOfCalls: 10             # No evaluar hasta tener al menos 10 llamadas en la ventana
          permittedNumberOfCallsInHalfOpenState: 3 # Permitir 3 llamadas de prueba en estado semi-abierto
          waitDurationInOpenState: 10s         # Mantener circuito abierto por 10 segundos antes de ir a semi-abierto
          automaticTransitionFromOpenToHalfOpenEnabled: true # Transición automática a semi-abierto
          # Qué excepciones cuentan como fallo (puedes ser más específico)
          ignoreExceptions:                    # Excepciones a IGNORAR (no cuentan como fallo)
            - com.mipaquete.exceptions.BusinessValidationException
          recordExceptions:                    # Excepciones a REGISTRAR como fallo (por defecto, todas las RuntimeException)
            - java.io.IOException
            - org.springframework.web.client.HttpServerErrorException
    */
}

// Necesitarás la dependencia: org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j
// Y posiblemente spring-boot-starter-actuator para el health check y spring-boot-starter-aop.

PRO Tip: El fallbackMethod es tu salvavidas. Define una estrategia de fallback inteligente. Puedes devolver datos de caché?, un valor por defecto?, una respuesta vacía pero válida?, o simplemente un error claro para el usuario!!! Depende del caso de uso. Y ojo!!! Configurar bien los umbrales (failureRateThreshold, slidingWindowSize, waitDurationInOpenState) es clave. Si eres muy sensible, el circuito se abrirá por cualquier tontería. Si eres muy permisivo, no te protegerá cuando de verdad lo necesites. Monitorea y ajusta, es la manera, o como decimos en Venezuela, probando es que se guisa!!!.

Diagrama del Patrón Circuit Breaker


Como ven muchachones, Spring Boot nos da las herramientas para aplicar estos patrones de sistemas distribuidos de forma bastante elegante. Desde partir tu monolito en Microservicios, hacer que se comuniquen chévere con Event-Driven, optimizar lecturas y escrituras con CQRS, poner orden con un API Gateway, manejar transas complejas con Sagas, hasta protegerte de fallos en cascada con Circuit Breakers.

Dominar estos patrones no es solo para lucir PRO (aunque ayuda 😉 jaja jaja), es fundamental para construir las aplicaciones robustas, escalables y resilientes que el mundo moderno exige. Spring Boot te quita mucho del trabajo pesado de infraestructura, para que tú te concentres en la lógica que aporta valor, hacer esa logica de negocio que produzca billetico...

Claro, cada patrón tiene sus pros y contras, y saber cuándo aplicar cuál, es parte del arte de ser un buen arquitecto o desarrollador senior. No hay balas de plata, ni atajos, hay es que analizar el problema, los requisitos, la complejidad y elegir la herramienta adecuada, asi que activitos muchachones...

👁️ 98 vistas