# 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. - Published: 2026-03-20 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring cloud gateway, microservices, api gateway, routing, technical interview - Reading time: 15 min --- 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. ```java // GatewayApplication.java // 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. ```java // RouteConfiguration.java // 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/(?.*)", "/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 ``` ```java // CustomPredicateFactory.java // Creación de un predicate personalizado @Component public class ApiKeyRoutePredicateFactory extends AbstractRoutePredicateFactory { public ApiKeyRoutePredicateFactory() { super(Config.class); } @Override public Predicate 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 validKeys = new ArrayList<>(); public List getValidKeys() { return validKeys; } public void setValidKeys(List 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. ```java // LoggingFilter.java // Filtro global de logging @Component @Slf4j public class LoggingFilter implements GlobalFilter, Ordered { @Override public Mono 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; } } ``` ```java // AuthenticationFilter.java // Filtro de autenticación JWT @Component @RequiredArgsConstructor public class AuthenticationFilter implements GatewayFilter { private final JwtTokenValidator tokenValidator; @Override public Mono 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 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/(?.*), /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}" ``` ```java // RateLimiterConfiguration.java // 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() ); } } ``` ### 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. ```java // RequestBodyModificationFilter.java // 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 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 body = objectMapper.readValue( bytes, new TypeReference>() {} ); // 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 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; } } ``` ```java // ResponseBodyModificationConfig.java // 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/ ``` ```java // LoadBalancerConfiguration.java // 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 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); } } ``` ```java // WeightedLoadBalancer.java // Load balancer ponderado @Component @RequiredArgsConstructor public class WeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer { private final String serviceId; private final ObjectProvider supplierProvider; private final Random random = new Random(); @Override public Mono> 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 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 ``` ```java // FallbackController.java // Controlador de fallback @RestController @RequestMapping("/fallback") @Slf4j public class FallbackController { @GetMapping("/orders") public Mono>> ordersFallback( ServerWebExchange exchange ) { log.warn("Circuit breaker activated for orders service"); Map 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>> ordersPostFallback() { Map 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)); } } ``` ```java // CircuitBreakerEventListener.java // 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 ``` ```java // CustomRetryFilter.java // 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 filter(ServerWebExchange exchange, GatewayFilterChain chain) { return Mono.defer(() -> attemptRequest(exchange, chain, 0)); } private Mono 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. ```java // AggregationController.java // 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 getUserDashboard(@PathVariable Long userId) { // Llamadas paralelas a distintos servicios Mono userMono = fetchUserProfile(userId); Mono> ordersMono = fetchRecentOrders(userId); Mono 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 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> 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 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 handleAggregationError(Throwable error) { log.error("Dashboard aggregation failed: {}", error.getMessage()); return Mono.just(DashboardResponse.builder() .error("Partial data available") .generatedAt(Instant.now()) .build()); } } ``` ```java // DashboardResponse.java // DTO de respuesta agregada @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DashboardResponse { private UserProfile user; private List 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 ``` ```java // SecurityConfig.java // 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; } } ``` ```java // TokenRelayFilter.java // Reenvío del token a los servicios downstream @Component public class TokenRelayFilter implements GlobalFilter, Ordered { @Override public Mono 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; } } ``` ### 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 ``` ```java // MetricsFilter.java // Filtro de métricas personalizadas @Component @RequiredArgsConstructor @Slf4j public class MetricsFilter implements GlobalFilter, Ordered { private final MeterRegistry meterRegistry; @Override public Mono 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; } } ``` ```java // TracingFilter.java // Propagación del contexto de tracing @Component @RequiredArgsConstructor public class TracingFilter implements GlobalFilter, Ordered { private final Tracer tracer; @Override public Mono 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/spring-boot/spring-cloud-gateway-interview-routing-filters