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 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.
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.
// 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);
}
}# 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.
// 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:
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 clientePregunta 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.
# 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// 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;
}
}
}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.
// 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;
}
}// 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.
# 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}"// 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.
// 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;
}
}// 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.
# 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/// 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);
}
}// 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.
# 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// 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));
}
}// 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());
}
}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.
# 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// 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.
// 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());
}
}// 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.
# 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// 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;
}
}// 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.
# 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// 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;
}
}// 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:
| 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 |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
Compartir
Artículos relacionados

Spring Kafka: arquitectura event-driven con consumidores resilientes
Guía completa de Spring Kafka para arquitecturas event-driven. Configuración, consumidores resilientes, políticas de retry, dead letter queues y patrones de producción para aplicaciones distribuidas.

Logging en Spring Boot 2026: logs estructurados en producción con Logback y JSON
Guía completa sobre logs estructurados en Spring Boot. Configuración Logback JSON, MDC para tracing, mejores prácticas en producción e integración con ELK Stack.

Entrevista Spring GraphQL: Resolvers, DataLoaders y Soluciones al Problema N+1
Prepárate para entrevistas Spring GraphQL con esta guía completa. Resolvers, DataLoaders, gestión del problema N+1, mutaciones y mejores prácticas para preguntas técnicas.