Entrevista Spring Cloud Gateway: Routing, Filtros y Load Balancing

Domina Spring Cloud Gateway para entrevistas técnicas: 12 preguntas sobre routing, filtros, load balancing y patrones API Gateway con ejemplos de código.

Spring Cloud Gateway: preguntas de entrevista sobre routing, filtros y load balancing

Spring Cloud Gateway es la solución de referencia para implementar un API Gateway en una arquitectura de microservicios Spring. Las entrevistas técnicas evalúan la capacidad de configurar el routing, crear filtros personalizados y gestionar el load balancing de forma eficaz.

Consejo de preparación

Los reclutadores ponen a prueba la comprensión de los patrones Gateway: autenticación centralizada, rate limiting y circuit breaker. Saber explicar por qué Spring Cloud Gateway frente a sus alternativas marca la diferencia.

Arquitectura y fundamentos de Spring Cloud Gateway

Pregunta 1: ¿Qué es Spring Cloud Gateway y por qué utilizarlo?

Spring Cloud Gateway es un API Gateway reactivo construido sobre Spring WebFlux y Project Reactor. Sirve como punto de entrada único para todas las peticiones hacia los microservicios, ofreciendo capacidades de routing, filtrado y load balancing.

GatewayApplication.javajava
// Configuración básica de Spring Cloud Gateway
@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        // Arranca el servidor reactivo Netty (no Tomcat)
        SpringApplication.run(GatewayApplication.class, args);
    }
}
yaml
# application.yml
# Configuración mínima del gateway
spring:
  cloud:
    gateway:
      routes:
        # Ruta hacia el servicio de usuarios
        - id: user-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/users/**
        # Ruta hacia el servicio de pedidos
        - id: order-service
          uri: http://localhost:8082
          predicates:
            - Path=/api/orders/**

Las principales ventajas de Spring Cloud Gateway incluyen: arquitectura no bloqueante para alto rendimiento, integración nativa con el ecosistema Spring Cloud y soporte para patrones reactivos modernos.

Pregunta 2: Explica los conceptos de Route, Predicate y Filter

Tres conceptos fundamentales estructuran Spring Cloud Gateway: las Routes definen los destinos, los Predicates determinan cuándo aplicar una ruta y los Filters modifican las peticiones y respuestas.

RouteConfiguration.javajava
// Configuración de rutas mediante código
@Configuration
public class RouteConfiguration {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // Ruta con varios predicates
            .route("product-service", r -> r
                // Predicate: ruta URL
                .path("/api/products/**")
                // Predicate: método HTTP
                .and()
                .method(HttpMethod.GET, HttpMethod.POST)
                // Predicate: cabecera presente
                .and()
                .header("X-Api-Version", "v2")
                // Filter: reescritura de ruta
                .filters(f -> f
                    .rewritePath("/api/products/(?<segment>.*)", "/products/${segment}")
                    // Filter: añadir cabecera
                    .addRequestHeader("X-Gateway-Source", "spring-cloud-gateway")
                )
                // URI de destino
                .uri("http://localhost:8083"))
            .build();
    }
}

El flujo de procesamiento sigue esta secuencia:

text
Petición entrante
┌─────────────────┐
│   Predicates    │ → Evalúa condiciones (path, método, header...)
└────────┬────────┘
         │ Coincidencia encontrada
┌─────────────────┐
│  Pre-Filters    │ → Modifica la petición antes del routing
└────────┬────────┘
┌─────────────────┐
│   HTTP Proxy    │ → Reenvía al servicio destino
└────────┬────────┘
┌─────────────────┐
│  Post-Filters   │ → Modifica la respuesta antes de devolverla al cliente
└────────┬────────┘
   Respuesta al cliente

Pregunta 3: ¿Cuáles son los predicates más utilizados?

Spring Cloud Gateway proporciona numerosos predicates integrados para condiciones de routing variadas. La combinación de varios predicates permite reglas de routing sofisticadas.

yaml
# application.yml
# Ejemplos de predicates comunes
spring:
  cloud:
    gateway:
      routes:
        # Routing por path con captura de variable
        - id: user-details
          uri: http://user-service
          predicates:
            - Path=/users/{userId}

        # Routing por método HTTP
        - id: user-create
          uri: http://user-service
          predicates:
            - Path=/users
            - Method=POST

        # Routing basado en cabeceras
        - id: mobile-api
          uri: http://mobile-service
          predicates:
            - Header=X-Client-Type, mobile

        # Routing por parámetros de query
        - id: search-api
          uri: http://search-service
          predicates:
            - Query=q

        # Routing basado en host
        - id: admin-portal
          uri: http://admin-service
          predicates:
            - Host=admin.example.com

        # Routing temporal
        - id: maintenance-mode
          uri: http://maintenance-service
          predicates:
            - Between=2026-03-20T02:00:00Z,2026-03-20T04:00:00Z
CustomPredicateFactory.javajava
// Creación de un predicate personalizado
@Component
public class ApiKeyRoutePredicateFactory
    extends AbstractRoutePredicateFactory<ApiKeyRoutePredicateFactory.Config> {

    public ApiKeyRoutePredicateFactory() {
        super(Config.class);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            // Verifica la presencia y validez de la API key
            String apiKey = exchange.getRequest()
                .getHeaders()
                .getFirst("X-Api-Key");

            return apiKey != null && config.getValidKeys().contains(apiKey);
        };
    }

    @Validated
    public static class Config {
        private List<String> validKeys = new ArrayList<>();

        public List<String> getValidKeys() {
            return validKeys;
        }

        public void setValidKeys(List<String> validKeys) {
            this.validKeys = validKeys;
        }
    }
}
Orden de los predicates

El orden de los predicates no afecta a la evaluación, pero el orden de las rutas sí importa. Las rutas se evalúan secuencialmente y se utiliza la primera coincidencia.

Filtros y transformación de peticiones

Pregunta 4: ¿Cómo funcionan los filtros pre y post-procesamiento?

Los filtros GatewayFilter se ejecutan en una cadena ordenada. Los filtros «pre» modifican la petición antes del routing y los filtros «post» modifican la respuesta tras recibirla del servicio destino.

LoggingFilter.javajava
// Filtro global de logging
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // PRE-FILTER: antes del routing
        String requestId = UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();

        log.info("Request {} started: {} {}",
            requestId,
            exchange.getRequest().getMethod(),
            exchange.getRequest().getPath());

        // Añade el ID de petición a las cabeceras
        ServerHttpRequest modifiedRequest = exchange.getRequest()
            .mutate()
            .header("X-Request-Id", requestId)
            .build();

        // Continúa la cadena y procesa la respuesta
        return chain.filter(exchange.mutate().request(modifiedRequest).build())
            .then(Mono.fromRunnable(() -> {
                // POST-FILTER: tras la respuesta
                long duration = System.currentTimeMillis() - startTime;
                HttpStatusCode status = exchange.getResponse().getStatusCode();

                log.info("Request {} completed: status={}, duration={}ms",
                    requestId, status, duration);
            }));
    }

    @Override
    public int getOrder() {
        // Orden negativo = se ejecuta primero
        return -1;
    }
}
AuthenticationFilter.javajava
// Filtro de autenticación JWT
@Component
@RequiredArgsConstructor
public class AuthenticationFilter implements GatewayFilter {

    private final JwtTokenValidator tokenValidator;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String authHeader = exchange.getRequest()
            .getHeaders()
            .getFirst(HttpHeaders.AUTHORIZATION);

        // Verifica la presencia del token
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return handleUnauthorized(exchange, "Missing or invalid Authorization header");
        }

        String token = authHeader.substring(7);

        // Valida el token de forma reactiva
        return tokenValidator.validate(token)
            .flatMap(claims -> {
                // Enriquece la petición con información del usuario
                ServerHttpRequest enrichedRequest = exchange.getRequest()
                    .mutate()
                    .header("X-User-Id", claims.getSubject())
                    .header("X-User-Roles", String.join(",", claims.getRoles()))
                    .build();

                return chain.filter(exchange.mutate().request(enrichedRequest).build());
            })
            .onErrorResume(e -> handleUnauthorized(exchange, e.getMessage()));
    }

    private Mono<Void> handleUnauthorized(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);

        String body = String.format("{\"error\": \"%s\"}", message);
        DataBuffer buffer = exchange.getResponse()
            .bufferFactory()
            .wrap(body.getBytes(StandardCharsets.UTF_8));

        return exchange.getResponse().writeWith(Mono.just(buffer));
    }
}

Pregunta 5: ¿Cuáles son los filtros integrados más útiles?

Spring Cloud Gateway proporciona muchos filtros integrados que cubren casos de uso comunes: reescritura de URL, modificación de cabeceras, retry y circuit breaker.

yaml
# application.yml
# Filtros integrados comunes
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            # Reescritura de path
            - RewritePath=/api/orders/(?<segment>.*), /orders/${segment}

            # Añade cabeceras de petición
            - AddRequestHeader=X-Gateway-Version, 1.0

            # Elimina cabeceras sensibles de la respuesta
            - RemoveResponseHeader=X-Powered-By
            - RemoveResponseHeader=Server

            # Prefijo de path
            - PrefixPath=/v2

            # Elimina prefijo
            - StripPrefix=1

            # Reintento automático en caso de error
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
                methods: GET
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 500ms
                  factor: 2

            # Limitación de tasa
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@userKeyResolver}"
RateLimiterConfiguration.javajava
// Configuración del rate limiter por usuario
@Configuration
public class RateLimiterConfiguration {

    @Bean
    public KeyResolver userKeyResolver() {
        // Limitación por usuario autenticado
        return exchange -> Mono.just(
            exchange.getRequest()
                .getHeaders()
                .getFirst("X-User-Id")
        ).defaultIfEmpty("anonymous");
    }

    @Bean
    public KeyResolver ipKeyResolver() {
        // Limitación por dirección IP
        return exchange -> Mono.just(
            Objects.requireNonNull(exchange.getRequest()
                .getRemoteAddress())
                .getAddress()
                .getHostAddress()
        );
    }
}

¿Listo para aprobar tus entrevistas de Spring Boot?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Pregunta 6: ¿Cómo implementar un filtro de modificación del body?

La modificación del cuerpo de la petición o respuesta requiere un enfoque específico con ModifyRequestBodyGatewayFilterFactory o ModifyResponseBodyGatewayFilterFactory.

RequestBodyModificationFilter.javajava
// Filtro de modificación del body de la petición
@Component
@RequiredArgsConstructor
public class RequestBodyModificationFilter implements GlobalFilter, Ordered {

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Solo modifica las peticiones POST/PUT con JSON
        if (!isJsonRequest(exchange)) {
            return chain.filter(exchange);
        }

        return DataBufferUtils.join(exchange.getRequest().getBody())
            .flatMap(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);

                try {
                    // Parsea y modifica el JSON
                    Map<String, Object> body = objectMapper.readValue(
                        bytes,
                        new TypeReference<Map<String, Object>>() {}
                    );

                    // Añade metadatos
                    body.put("processedAt", Instant.now().toString());
                    body.put("gatewayVersion", "1.0");

                    byte[] modifiedBytes = objectMapper.writeValueAsBytes(body);

                    // Crea una nueva petición con el body modificado
                    ServerHttpRequest modifiedRequest = new ServerHttpRequestDecorator(
                        exchange.getRequest()
                    ) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return Flux.just(
                                exchange.getResponse()
                                    .bufferFactory()
                                    .wrap(modifiedBytes)
                            );
                        }

                        @Override
                        public HttpHeaders getHeaders() {
                            HttpHeaders headers = new HttpHeaders();
                            headers.putAll(super.getHeaders());
                            headers.setContentLength(modifiedBytes.length);
                            return headers;
                        }
                    };

                    return chain.filter(exchange.mutate().request(modifiedRequest).build());

                } catch (IOException e) {
                    return Mono.error(new RuntimeException("Failed to parse request body", e));
                }
            });
    }

    private boolean isJsonRequest(ServerWebExchange exchange) {
        MediaType contentType = exchange.getRequest().getHeaders().getContentType();
        return contentType != null &&
               contentType.isCompatibleWith(MediaType.APPLICATION_JSON);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
ResponseBodyModificationConfig.javajava
// Configuración para modificar las respuestas
@Configuration
public class ResponseBodyModificationConfig {

    @Bean
    public RouteLocator responseModifyingRoutes(
        RouteLocatorBuilder builder,
        ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilter
    ) {
        return builder.routes()
            .route("modify-response", r -> r
                .path("/api/users/**")
                .filters(f -> f.modifyResponseBody(
                    String.class,
                    String.class,
                    MediaType.APPLICATION_JSON_VALUE,
                    (exchange, responseBody) -> {
                        // Envuelve la respuesta en un formato estándar
                        return Mono.just(String.format(
                            "{\"success\": true, \"data\": %s, \"timestamp\": \"%s\"}",
                            responseBody,
                            Instant.now()
                        ));
                    }
                ))
                .uri("lb://user-service"))
            .build();
    }
}

Load Balancing y resiliencia

Pregunta 7: ¿Cómo configurar el load balancing con Spring Cloud LoadBalancer?

Spring Cloud Gateway se integra con Spring Cloud LoadBalancer para distribuir el tráfico entre instancias de servicio. El esquema URI lb:// activa el load balancing automático.

yaml
# application.yml
# Configuración del load balancing
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          # lb:// activa el load balancing
          uri: lb://user-service
          predicates:
            - Path=/api/users/**

    # Configuración del load balancer
    loadbalancer:
      ribbon:
        enabled: false  # Usa Spring Cloud LoadBalancer (no Ribbon)

      # Configuración por servicio
      configurations: default

      # Health check para el load balancing
      health-check:
        path:
          user-service: /actuator/health
        interval: 10s

# Service discovery (Eureka u otro)
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
LoadBalancerConfiguration.javajava
// Configuración personalizada del load balancer
@Configuration
@LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class)
public class LoadBalancerConfiguration {
}

// CustomLoadBalancerConfig.java
// Estrategia personalizada de load balancing
public class CustomLoadBalancerConfig {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> loadBalancer(
        Environment environment,
        LoadBalancerClientFactory clientFactory
    ) {
        String serviceId = environment.getProperty(
            LoadBalancerClientFactory.PROPERTY_NAME
        );

        // Round Robin por defecto
        return new RoundRobinLoadBalancer(
            clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
            serviceId
        );
    }

    @Bean
    public ServiceInstanceListSupplier serviceInstanceListSupplier(
        ConfigurableApplicationContext context
    ) {
        // Añade health check a las instancias
        return ServiceInstanceListSupplier.builder()
            .withDiscoveryClient()
            .withHealthChecks()
            .withCaching()
            .build(context);
    }
}
WeightedLoadBalancer.javajava
// Load balancer ponderado
@Component
@RequiredArgsConstructor
public class WeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final String serviceId;
    private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;
    private final Random random = new Random();

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        return supplierProvider.getIfAvailable()
            .get()
            .next()
            .map(instances -> {
                if (instances.isEmpty()) {
                    return new EmptyResponse();
                }

                // Calcula los pesos a partir de los metadatos
                List<WeightedInstance> weighted = instances.stream()
                    .map(instance -> {
                        int weight = Integer.parseInt(
                            instance.getMetadata().getOrDefault("weight", "1")
                        );
                        return new WeightedInstance(instance, weight);
                    })
                    .toList();

                // Selección ponderada
                int totalWeight = weighted.stream()
                    .mapToInt(WeightedInstance::weight)
                    .sum();

                int randomWeight = random.nextInt(totalWeight);
                int currentWeight = 0;

                for (WeightedInstance wi : weighted) {
                    currentWeight += wi.weight();
                    if (randomWeight < currentWeight) {
                        return new DefaultResponse(wi.instance());
                    }
                }

                return new DefaultResponse(weighted.get(0).instance());
            });
    }

    private record WeightedInstance(ServiceInstance instance, int weight) {}
}

Pregunta 8: ¿Cómo implementar un circuit breaker en el gateway?

El circuit breaker protege contra los fallos en cascada. Spring Cloud Gateway se integra con Resilience4j para una gestión avanzada de fallos.

yaml
# application.yml
# Configuración del circuit breaker Resilience4j
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            # Circuit breaker con fallback
            - name: CircuitBreaker
              args:
                name: orderServiceCB
                fallbackUri: forward:/fallback/orders

resilience4j:
  circuitbreaker:
    configs:
      default:
        # Número de peticiones evaluadas
        slidingWindowSize: 10
        # Umbral de fallos para abrir el circuito
        failureRateThreshold: 50
        # Duración con el circuito abierto antes de reintentar
        waitDurationInOpenState: 30s
        # Peticiones permitidas en half-open
        permittedNumberOfCallsInHalfOpenState: 3
        # Transiciones automáticas
        automaticTransitionFromOpenToHalfOpenEnabled: true

    instances:
      orderServiceCB:
        baseConfig: default
        failureRateThreshold: 60

  timelimiter:
    configs:
      default:
        timeoutDuration: 5s

    instances:
      orderServiceCB:
        timeoutDuration: 3s
FallbackController.javajava
// Controlador de fallback
@RestController
@RequestMapping("/fallback")
@Slf4j
public class FallbackController {

    @GetMapping("/orders")
    public Mono<ResponseEntity<Map<String, Object>>> ordersFallback(
        ServerWebExchange exchange
    ) {
        log.warn("Circuit breaker activated for orders service");

        Map<String, Object> response = Map.of(
            "success", false,
            "error", "Service temporarily unavailable",
            "code", "SERVICE_UNAVAILABLE",
            "retryAfter", 30
        );

        return Mono.just(ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(response));
    }

    @PostMapping("/orders")
    public Mono<ResponseEntity<Map<String, Object>>> ordersPostFallback() {
        Map<String, Object> response = Map.of(
            "success", false,
            "error", "Order creation temporarily unavailable",
            "code", "SERVICE_UNAVAILABLE",
            "message", "Please try again later"
        );

        return Mono.just(ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(response));
    }
}
CircuitBreakerEventListener.javajava
// Monitorización de eventos del circuit breaker
@Component
@Slf4j
@RequiredArgsConstructor
public class CircuitBreakerEventListener {

    private final MeterRegistry meterRegistry;

    @EventListener
    public void onCircuitBreakerStateTransition(
        CircuitBreakerOnStateTransitionEvent event
    ) {
        CircuitBreaker.StateTransition transition = event.getStateTransition();

        log.info("Circuit breaker {} state changed: {} -> {}",
            event.getCircuitBreakerName(),
            transition.getFromState(),
            transition.getToState());

        // Métrica Micrometer
        meterRegistry.counter(
            "circuit_breaker.state_transition",
            "name", event.getCircuitBreakerName(),
            "from", transition.getFromState().name(),
            "to", transition.getToState().name()
        ).increment();
    }

    @EventListener
    public void onCircuitBreakerFailure(CircuitBreakerOnErrorEvent event) {
        log.error("Circuit breaker {} error: {}",
            event.getCircuitBreakerName(),
            event.getThrowable().getMessage());
    }
}
Timeout y circuit breaker

Configura un timeout coherente entre el circuit breaker y el cliente HTTP. Un timeout demasiado largo bloquea hilos, mientras que uno demasiado corto provoca falsos positivos.

Pregunta 9: ¿Cómo implementar reintentos con backoff exponencial?

Un reintento inteligente con backoff exponencial evita saturar un servicio en dificultades, maximizando al mismo tiempo las posibilidades de éxito.

yaml
# application.yml
# Configuración de retry con backoff
spring:
  cloud:
    gateway:
      routes:
        - id: payment-service
          uri: lb://payment-service
          predicates:
            - Path=/api/payments/**
          filters:
            - name: Retry
              args:
                # Número de intentos
                retries: 3
                # Códigos HTTP que disparan el retry
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT
                # Solo métodos HTTP idempotentes
                methods: GET,PUT
                # Excepciones que disparan el retry
                exceptions:
                  - java.io.IOException
                  - java.net.ConnectException
                  - org.springframework.cloud.gateway.support.TimeoutException
                # Backoff exponencial
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 2000ms
                  factor: 2
                  basedOnPreviousValue: true
CustomRetryFilter.javajava
// Retry personalizado con lógica de negocio
@Component
@Slf4j
public class CustomRetryFilter implements GatewayFilter, Ordered {

    private static final int MAX_RETRIES = 3;
    private static final Duration INITIAL_BACKOFF = Duration.ofMillis(100);
    private static final double BACKOFF_MULTIPLIER = 2.0;
    private static final double JITTER_FACTOR = 0.1;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return Mono.defer(() -> attemptRequest(exchange, chain, 0));
    }

    private Mono<Void> attemptRequest(
        ServerWebExchange exchange,
        GatewayFilterChain chain,
        int attempt
    ) {
        return chain.filter(exchange)
            .onErrorResume(throwable -> {
                if (attempt >= MAX_RETRIES || !isRetryable(throwable, exchange)) {
                    return Mono.error(throwable);
                }

                Duration backoff = calculateBackoff(attempt);
                log.warn("Retry attempt {} after {}ms for {} {}",
                    attempt + 1,
                    backoff.toMillis(),
                    exchange.getRequest().getMethod(),
                    exchange.getRequest().getPath());

                return Mono.delay(backoff)
                    .then(Mono.defer(() ->
                        attemptRequest(exchange, chain, attempt + 1)
                    ));
            });
    }

    private boolean isRetryable(Throwable throwable, ServerWebExchange exchange) {
        // Solo reintenta los métodos idempotentes
        HttpMethod method = exchange.getRequest().getMethod();
        if (!Set.of(HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE)
            .contains(method)) {
            return false;
        }

        // Verifica el tipo de excepción
        return throwable instanceof ConnectException ||
               throwable instanceof TimeoutException ||
               throwable instanceof ServiceUnavailableException;
    }

    private Duration calculateBackoff(int attempt) {
        // Backoff exponencial con jitter
        long baseBackoff = (long) (
            INITIAL_BACKOFF.toMillis() * Math.pow(BACKOFF_MULTIPLIER, attempt)
        );

        // Añade jitter aleatorio (±10%)
        double jitter = 1.0 + (Math.random() - 0.5) * 2 * JITTER_FACTOR;
        long finalBackoff = (long) (baseBackoff * jitter);

        return Duration.ofMillis(finalBackoff);
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 1;
    }
}

Patrones avanzados y buenas prácticas

Pregunta 10: ¿Cómo implementar la agregación de peticiones?

La agregación combina varias llamadas a microservicios en una única respuesta para el cliente, reduciendo la latencia y la complejidad del frontend.

AggregationController.javajava
// Agregación multi-servicio
@RestController
@RequestMapping("/api/aggregate")
@RequiredArgsConstructor
@Slf4j
public class AggregationController {

    private final WebClient.Builder webClientBuilder;
    private final CircuitBreakerRegistry circuitBreakerRegistry;

    @GetMapping("/user-dashboard/{userId}")
    public Mono<DashboardResponse> getUserDashboard(@PathVariable Long userId) {
        // Llamadas paralelas a distintos servicios
        Mono<UserProfile> userMono = fetchUserProfile(userId);
        Mono<List<Order>> ordersMono = fetchRecentOrders(userId);
        Mono<NotificationCount> notificationsMono = fetchNotificationCount(userId);

        // Agregación de los resultados
        return Mono.zip(userMono, ordersMono, notificationsMono)
            .map(tuple -> DashboardResponse.builder()
                .user(tuple.getT1())
                .recentOrders(tuple.getT2())
                .notificationCount(tuple.getT3())
                .generatedAt(Instant.now())
                .build())
            .timeout(Duration.ofSeconds(5))
            .onErrorResume(this::handleAggregationError);
    }

    private Mono<UserProfile> fetchUserProfile(Long userId) {
        return webClientBuilder.build()
            .get()
            .uri("lb://user-service/users/{id}", userId)
            .retrieve()
            .bodyToMono(UserProfile.class)
            .transform(CircuitBreakerOperator.of(
                circuitBreakerRegistry.circuitBreaker("user-service")
            ))
            .onErrorReturn(new UserProfile(userId, "Unknown", null));
    }

    private Mono<List<Order>> fetchRecentOrders(Long userId) {
        return webClientBuilder.build()
            .get()
            .uri("lb://order-service/orders?userId={id}&limit=5", userId)
            .retrieve()
            .bodyToFlux(Order.class)
            .collectList()
            .transform(CircuitBreakerOperator.of(
                circuitBreakerRegistry.circuitBreaker("order-service")
            ))
            .onErrorReturn(Collections.emptyList());
    }

    private Mono<NotificationCount> fetchNotificationCount(Long userId) {
        return webClientBuilder.build()
            .get()
            .uri("lb://notification-service/notifications/count/{id}", userId)
            .retrieve()
            .bodyToMono(NotificationCount.class)
            .transform(CircuitBreakerOperator.of(
                circuitBreakerRegistry.circuitBreaker("notification-service")
            ))
            .onErrorReturn(new NotificationCount(0, 0));
    }

    private Mono<DashboardResponse> handleAggregationError(Throwable error) {
        log.error("Dashboard aggregation failed: {}", error.getMessage());
        return Mono.just(DashboardResponse.builder()
            .error("Partial data available")
            .generatedAt(Instant.now())
            .build());
    }
}
DashboardResponse.javajava
// DTO de respuesta agregada
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardResponse {
    private UserProfile user;
    private List<Order> recentOrders;
    private NotificationCount notificationCount;
    private Instant generatedAt;
    private String error;
}

Pregunta 11: ¿Cómo asegurar el gateway con OAuth2?

La integración OAuth2 centraliza la autenticación a nivel del gateway, evitando la duplicación de lógica en cada microservicio.

yaml
# application.yml
# Configuración OAuth2 Resource Server
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/myrealm
          jwk-set-uri: https://auth.example.com/realms/myrealm/protocol/openid-connect/certs
SecurityConfig.javajava
// Configuración de seguridad del gateway
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        return http
            // Desactiva CSRF para una API sin estado
            .csrf(ServerHttpSecurity.CsrfSpec::disable)

            // Configuración de la autorización
            .authorizeExchange(exchanges -> exchanges
                // Endpoints públicos
                .pathMatchers("/actuator/health", "/actuator/info").permitAll()
                .pathMatchers("/api/public/**").permitAll()
                .pathMatchers("/api/auth/**").permitAll()

                // Endpoints específicos por rol
                .pathMatchers("/api/admin/**").hasRole("ADMIN")
                .pathMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")

                // Todo el resto requiere autenticación
                .anyExchange().authenticated()
            )

            // OAuth2 Resource Server con JWT
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            )

            .build();
    }

    @Bean
    public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
            new JwtGrantedAuthoritiesConverter();
        // Extrae los roles del claim "roles"
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

        ReactiveJwtAuthenticationConverter converter =
            new ReactiveJwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(
            new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter)
        );

        return converter;
    }
}
TokenRelayFilter.javajava
// Reenvío del token a los servicios downstream
@Component
public class TokenRelayFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return ReactiveSecurityContextHolder.getContext()
            .map(SecurityContext::getAuthentication)
            .filter(auth -> auth instanceof JwtAuthenticationToken)
            .cast(JwtAuthenticationToken.class)
            .map(JwtAuthenticationToken::getToken)
            .map(jwt -> {
                // Reenvía el token a los servicios downstream
                ServerHttpRequest request = exchange.getRequest()
                    .mutate()
                    .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt.getTokenValue())
                    // Añade información del usuario extraída del token
                    .header("X-User-Id", jwt.getSubject())
                    .header("X-User-Email", jwt.getClaimAsString("email"))
                    .build();

                return exchange.mutate().request(request).build();
            })
            .defaultIfEmpty(exchange)
            .flatMap(chain::filter);
    }

    @Override
    public int getOrder() {
        // Tras la autenticación, antes del routing
        return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() + 1;
    }
}

¿Listo para aprobar tus entrevistas de Spring Boot?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Pregunta 12: ¿Cuáles son las buenas prácticas de monitorización y observabilidad?

La monitorización del gateway resulta crucial para identificar problemas de rendimiento y disponibilidad en una arquitectura de microservicios.

yaml
# application.yml
# Configuración de la observabilidad
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,gateway
  metrics:
    tags:
      application: api-gateway
    distribution:
      percentiles-histogram:
        http.server.requests: true
      percentiles:
        http.server.requests: 0.5, 0.95, 0.99
  tracing:
    sampling:
      probability: 1.0

spring:
  cloud:
    gateway:
      metrics:
        enabled: true
        tags:
          path:
            enabled: true
MetricsFilter.javajava
// Filtro de métricas personalizadas
@Component
@RequiredArgsConstructor
@Slf4j
public class MetricsFilter implements GlobalFilter, Ordered {

    private final MeterRegistry meterRegistry;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        long startTime = System.nanoTime();
        String path = exchange.getRequest().getPath().value();
        String method = exchange.getRequest().getMethod().name();

        // Extrae el servicio destino de la ruta
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        String routeId = route != null ? route.getId() : "unknown";

        return chain.filter(exchange)
            .doOnSuccess(v -> recordMetrics(exchange, startTime, routeId, "success"))
            .doOnError(e -> recordMetrics(exchange, startTime, routeId, "error"));
    }

    private void recordMetrics(
        ServerWebExchange exchange,
        long startTime,
        String routeId,
        String outcome
    ) {
        long duration = System.nanoTime() - startTime;
        HttpStatusCode status = exchange.getResponse().getStatusCode();
        String statusCode = status != null ? String.valueOf(status.value()) : "0";

        // Timer para la latencia
        Timer.builder("gateway.request.duration")
            .tag("route", routeId)
            .tag("method", exchange.getRequest().getMethod().name())
            .tag("status", statusCode)
            .tag("outcome", outcome)
            .register(meterRegistry)
            .record(duration, TimeUnit.NANOSECONDS);

        // Contador de peticiones
        meterRegistry.counter(
            "gateway.requests.total",
            "route", routeId,
            "status", statusCode
        ).increment();

        // Logs de peticiones lentas
        if (duration > TimeUnit.SECONDS.toNanos(1)) {
            log.warn("Slow request: {} {} - {}ms (route: {})",
                exchange.getRequest().getMethod(),
                exchange.getRequest().getPath(),
                TimeUnit.NANOSECONDS.toMillis(duration),
                routeId);
        }
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
TracingFilter.javajava
// Propagación del contexto de tracing
@Component
@RequiredArgsConstructor
public class TracingFilter implements GlobalFilter, Ordered {

    private final Tracer tracer;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Crea o recupera el span de tracing
        Span span = tracer.nextSpan()
            .name("gateway-request")
            .tag("http.method", exchange.getRequest().getMethod().name())
            .tag("http.url", exchange.getRequest().getURI().toString())
            .start();

        // Inyecta las cabeceras de tracing
        ServerHttpRequest request = exchange.getRequest()
            .mutate()
            .header("X-Trace-Id", span.context().traceId())
            .header("X-Span-Id", span.context().spanId())
            .build();

        return chain.filter(exchange.mutate().request(request).build())
            .doOnSuccess(v -> {
                HttpStatusCode status = exchange.getResponse().getStatusCode();
                span.tag("http.status_code",
                    status != null ? String.valueOf(status.value()) : "0");
                span.end();
            })
            .doOnError(e -> {
                span.tag("error", e.getMessage());
                span.end();
            });
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
}

Tabla de métricas esenciales:

text
| Métrica                           | Descripción                      |
|-----------------------------------|----------------------------------|
| gateway.request.duration          | Latencia por ruta/estado         |
| gateway.requests.total            | Contador de peticiones           |
| resilience4j.circuitbreaker.state | Estados del circuit breaker      |
| http.server.requests              | Métricas HTTP estándar           |
| spring.cloud.gateway.routes       | Rutas activas                    |
Dashboards recomendados

Utiliza Grafana con los dashboards de Spring Cloud Gateway y Resilience4j para visualizar las métricas. Configura alertas sobre la latencia P99 y la tasa de error.

Conclusión

Spring Cloud Gateway es un componente esencial de las arquitecturas de microservicios modernas. Puntos clave a recordar para las entrevistas:

Arquitectura y conceptos:

  • ✅ Routes, Predicates y Filters forman el modelo base
  • ✅ Arquitectura reactiva con WebFlux y Netty
  • ✅ Integración nativa con el ecosistema Spring Cloud

Funcionalidades esenciales:

  • ✅ Routing dinámico basado en múltiples criterios
  • ✅ Filtros pre/post para la transformación de peticiones y respuestas
  • ✅ Load balancing con Spring Cloud LoadBalancer

Resiliencia y seguridad:

  • ✅ Circuit breaker con Resilience4j y fallback
  • ✅ Retry con backoff exponencial y jitter
  • ✅ Autenticación OAuth2/JWT centralizada

Observabilidad:

  • ✅ Métricas Micrometer con tags por ruta
  • ✅ Tracing distribuido con propagación de contexto
  • ✅ Health checks y endpoints actuator

Dominar Spring Cloud Gateway demuestra una comprensión profunda de los patrones de microservicios y de las problemáticas de escalabilidad. Estas competencias son esenciales para diseñar API Gateways robustos y de alto rendimiento.

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Etiquetas

#spring cloud gateway
#microservices
#api gateway
#routing
#technical interview

Compartir

Artículos relacionados