Spring Cloud Gateway Interview: Routing, Filters and Load Balancing
Master Spring Cloud Gateway for technical interviews: 12 questions covering routing, filters, load balancing and API Gateway patterns with code examples.

Spring Cloud Gateway is the reference solution for implementing an API Gateway in a Spring microservices architecture. Technical interviews evaluate the ability to configure routing, create custom filters, and manage load balancing effectively.
Recruiters test understanding of Gateway patterns: centralized authentication, rate limiting, and circuit breaker. Being able to explain why Spring Cloud Gateway over alternatives makes the difference.
Architecture and Spring Cloud Gateway Fundamentals
Question 1: What is Spring Cloud Gateway and why use it?
Spring Cloud Gateway is a reactive API Gateway built on Spring WebFlux and Project Reactor. It serves as the single entry point for all requests to microservices, providing routing, filtering, and load balancing capabilities.
// Basic Spring Cloud Gateway setup
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
// Starts reactive Netty server (not Tomcat)
SpringApplication.run(GatewayApplication.class, args);
}
}# application.yml
# Minimal gateway configuration
spring:
cloud:
gateway:
routes:
# Route to user service
- id: user-service
uri: http://localhost:8081
predicates:
- Path=/api/users/**
# Route to order service
- id: order-service
uri: http://localhost:8082
predicates:
- Path=/api/orders/**The main advantages of Spring Cloud Gateway include: non-blocking architecture for high performance, native integration with the Spring Cloud ecosystem, and support for modern reactive patterns.
Question 2: Explain Route, Predicate, and Filter concepts
Three fundamental concepts structure Spring Cloud Gateway: Routes define destinations, Predicates determine when to apply a route, and Filters modify requests/responses.
// Programmatic route configuration
@Configuration
public class RouteConfiguration {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// Route with multiple predicates
.route("product-service", r -> r
// Predicate: URL path
.path("/api/products/**")
// Predicate: HTTP method
.and()
.method(HttpMethod.GET, HttpMethod.POST)
// Predicate: header present
.and()
.header("X-Api-Version", "v2")
// Filter: path rewrite
.filters(f -> f
.rewritePath("/api/products/(?<segment>.*)", "/products/${segment}")
// Filter: add header
.addRequestHeader("X-Gateway-Source", "spring-cloud-gateway")
)
// Destination URI
.uri("http://localhost:8083"))
.build();
}
}The processing flow follows this sequence:
Incoming request
│
▼
┌─────────────────┐
│ Predicates │ → Evaluate conditions (path, method, header...)
└────────┬────────┘
│ Match found
▼
┌─────────────────┐
│ Pre-Filters │ → Modify request before routing
└────────┬────────┘
│
▼
┌─────────────────┐
│ HTTP Proxy │ → Forward to target service
└────────┬────────┘
│
▼
┌─────────────────┐
│ Post-Filters │ → Modify response before returning to client
└────────┬────────┘
│
▼
Client responseQuestion 3: What are the most commonly used predicates?
Spring Cloud Gateway provides many built-in predicates for varied routing conditions. Combining multiple predicates enables sophisticated routing rules.
# application.yml
# Common predicate examples
spring:
cloud:
gateway:
routes:
# Path routing with variable capture
- id: user-details
uri: http://user-service
predicates:
- Path=/users/{userId}
# HTTP method routing
- id: user-create
uri: http://user-service
predicates:
- Path=/users
- Method=POST
# Header-based routing
- id: mobile-api
uri: http://mobile-service
predicates:
- Header=X-Client-Type, mobile
# Query parameter routing
- id: search-api
uri: http://search-service
predicates:
- Query=q
# Host-based routing
- id: admin-portal
uri: http://admin-service
predicates:
- Host=admin.example.com
# Time-based routing
- id: maintenance-mode
uri: http://maintenance-service
predicates:
- Between=2026-03-20T02:00:00Z,2026-03-20T04:00:00Z// Creating a custom predicate
@Component
public class ApiKeyRoutePredicateFactory
extends AbstractRoutePredicateFactory<ApiKeyRoutePredicateFactory.Config> {
public ApiKeyRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
// Check API key presence and validity
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;
}
}
}Predicate order does not affect evaluation, but route order matters. Routes are evaluated sequentially, and the first match is used.
Filters and Request Transformation
Question 4: How do pre and post-processing filters work?
GatewayFilter filters execute in an ordered chain. "Pre" filters modify the request before routing, "post" filters modify the response after receiving from the target service.
// Global logging filter
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// PRE-FILTER: before routing
String requestId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
log.info("Request {} started: {} {}",
requestId,
exchange.getRequest().getMethod(),
exchange.getRequest().getPath());
// Add request ID to headers
ServerHttpRequest modifiedRequest = exchange.getRequest()
.mutate()
.header("X-Request-Id", requestId)
.build();
// Continue the chain and handle response
return chain.filter(exchange.mutate().request(modifiedRequest).build())
.then(Mono.fromRunnable(() -> {
// POST-FILTER: after response
long duration = System.currentTimeMillis() - startTime;
HttpStatusCode status = exchange.getResponse().getStatusCode();
log.info("Request {} completed: status={}, duration={}ms",
requestId, status, duration);
}));
}
@Override
public int getOrder() {
// Negative order = executed first
return -1;
}
}// JWT authentication filter
@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);
// Check token presence
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return handleUnauthorized(exchange, "Missing or invalid Authorization header");
}
String token = authHeader.substring(7);
// Validate token reactively
return tokenValidator.validate(token)
.flatMap(claims -> {
// Enrich request with user information
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));
}
}Question 5: What are the most useful built-in filters?
Spring Cloud Gateway provides many built-in filters covering common use cases: URL rewriting, header modification, retry, and circuit breaker.
# application.yml
# Common built-in filters
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
# Path rewriting
- RewritePath=/api/orders/(?<segment>.*), /orders/${segment}
# Add request headers
- AddRequestHeader=X-Gateway-Version, 1.0
# Remove sensitive response headers
- RemoveResponseHeader=X-Powered-By
- RemoveResponseHeader=Server
# Path prefix
- PrefixPath=/v2
# Strip prefix
- StripPrefix=1
# Automatic retry on errors
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET
backoff:
firstBackoff: 100ms
maxBackoff: 500ms
factor: 2
# Rate limiting
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"// Per-user rate limiter configuration
@Configuration
public class RateLimiterConfiguration {
@Bean
public KeyResolver userKeyResolver() {
// Limit by authenticated user
return exchange -> Mono.just(
exchange.getRequest()
.getHeaders()
.getFirst("X-User-Id")
).defaultIfEmpty("anonymous");
}
@Bean
public KeyResolver ipKeyResolver() {
// Limit by IP address
return exchange -> Mono.just(
Objects.requireNonNull(exchange.getRequest()
.getRemoteAddress())
.getAddress()
.getHostAddress()
);
}
}Ready to ace your Spring Boot interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Question 6: How to implement a body modification filter?
Modifying the request or response body requires a specific approach with ModifyRequestBodyGatewayFilterFactory or ModifyResponseBodyGatewayFilterFactory.
// Request body modification filter
@Component
@RequiredArgsConstructor
public class RequestBodyModificationFilter implements GlobalFilter, Ordered {
private final ObjectMapper objectMapper;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Only modify POST/PUT requests with 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 {
// Parse and modify JSON
Map<String, Object> body = objectMapper.readValue(
bytes,
new TypeReference<Map<String, Object>>() {}
);
// Add metadata
body.put("processedAt", Instant.now().toString());
body.put("gatewayVersion", "1.0");
byte[] modifiedBytes = objectMapper.writeValueAsBytes(body);
// Create new request with modified body
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;
}
}// Configuration for modifying responses
@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) -> {
// Wrap response in standard format
return Mono.just(String.format(
"{\"success\": true, \"data\": %s, \"timestamp\": \"%s\"}",
responseBody,
Instant.now()
));
}
))
.uri("lb://user-service"))
.build();
}
}Load Balancing and Resilience
Question 7: How to configure load balancing with Spring Cloud LoadBalancer?
Spring Cloud Gateway integrates with Spring Cloud LoadBalancer to distribute traffic between service instances. The lb:// URI scheme activates automatic load balancing.
# application.yml
# Load balancing configuration
spring:
cloud:
gateway:
routes:
- id: user-service
# lb:// activates load balancing
uri: lb://user-service
predicates:
- Path=/api/users/**
# Load balancer configuration
loadbalancer:
ribbon:
enabled: false # Use Spring Cloud LoadBalancer (not Ribbon)
# Per-service configuration
configurations: default
# Health check for load balancing
health-check:
path:
user-service: /actuator/health
interval: 10s
# Service discovery (Eureka or other)
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/// Custom load balancer configuration
@Configuration
@LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class)
public class LoadBalancerConfiguration {
}
// CustomLoadBalancerConfig.java
// Custom load balancing strategy
public class CustomLoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> loadBalancer(
Environment environment,
LoadBalancerClientFactory clientFactory
) {
String serviceId = environment.getProperty(
LoadBalancerClientFactory.PROPERTY_NAME
);
// Use Round Robin by default
return new RoundRobinLoadBalancer(
clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
serviceId
);
}
@Bean
public ServiceInstanceListSupplier serviceInstanceListSupplier(
ConfigurableApplicationContext context
) {
// Add health check to instances
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withHealthChecks()
.withCaching()
.build(context);
}
}// Weighted load balancer
@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();
}
// Calculate weights based on metadata
List<WeightedInstance> weighted = instances.stream()
.map(instance -> {
int weight = Integer.parseInt(
instance.getMetadata().getOrDefault("weight", "1")
);
return new WeightedInstance(instance, weight);
})
.toList();
// Weighted selection
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) {}
}Question 8: How to implement circuit breaker in the gateway?
The circuit breaker protects against cascading failures. Spring Cloud Gateway integrates with Resilience4j for advanced failure management.
# application.yml
# Resilience4j Circuit Breaker configuration
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
# Circuit breaker with fallback
- name: CircuitBreaker
args:
name: orderServiceCB
fallbackUri: forward:/fallback/orders
resilience4j:
circuitbreaker:
configs:
default:
# Number of requests evaluated
slidingWindowSize: 10
# Failure threshold to open circuit
failureRateThreshold: 50
# Duration circuit stays open before retry
waitDurationInOpenState: 30s
# Requests allowed in half-open
permittedNumberOfCallsInHalfOpenState: 3
# Automatic transitions
automaticTransitionFromOpenToHalfOpenEnabled: true
instances:
orderServiceCB:
baseConfig: default
failureRateThreshold: 60
timelimiter:
configs:
default:
timeoutDuration: 5s
instances:
orderServiceCB:
timeoutDuration: 3s// Fallback controller
@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));
}
}// Circuit breaker event monitoring
@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());
// Micrometer metric
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());
}
}Configure consistent timeout between circuit breaker and HTTP client. A timeout that is too long blocks threads, while one that is too short triggers false positives.
Question 9: How to implement retry with exponential backoff?
Smart retry with exponential backoff avoids overwhelming a struggling service while maximizing success chances.
# application.yml
# Retry with backoff configuration
spring:
cloud:
gateway:
routes:
- id: payment-service
uri: lb://payment-service
predicates:
- Path=/api/payments/**
filters:
- name: Retry
args:
# Number of attempts
retries: 3
# HTTP codes triggering retry
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT
# Idempotent HTTP methods only
methods: GET,PUT
# Exceptions triggering retry
exceptions:
- java.io.IOException
- java.net.ConnectException
- org.springframework.cloud.gateway.support.TimeoutException
# Exponential backoff
backoff:
firstBackoff: 100ms
maxBackoff: 2000ms
factor: 2
basedOnPreviousValue: true// Custom retry with business logic
@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) {
// Only retry idempotent methods
HttpMethod method = exchange.getRequest().getMethod();
if (!Set.of(HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE)
.contains(method)) {
return false;
}
// Check exception type
return throwable instanceof ConnectException ||
throwable instanceof TimeoutException ||
throwable instanceof ServiceUnavailableException;
}
private Duration calculateBackoff(int attempt) {
// Exponential backoff with jitter
long baseBackoff = (long) (
INITIAL_BACKOFF.toMillis() * Math.pow(BACKOFF_MULTIPLIER, attempt)
);
// Add random jitter (±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;
}
}Advanced Patterns and Best Practices
Question 10: How to implement request aggregation?
Aggregation combines multiple microservice calls into a single response for the client, reducing latency and frontend complexity.
// Multi-service aggregation
@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) {
// Parallel calls to different services
Mono<UserProfile> userMono = fetchUserProfile(userId);
Mono<List<Order>> ordersMono = fetchRecentOrders(userId);
Mono<NotificationCount> notificationsMono = fetchNotificationCount(userId);
// Result aggregation
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());
}
}// Aggregated response DTO
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardResponse {
private UserProfile user;
private List<Order> recentOrders;
private NotificationCount notificationCount;
private Instant generatedAt;
private String error;
}Question 11: How to secure the gateway with OAuth2?
OAuth2 integration centralizes authentication at the gateway level, avoiding logic duplication in each microservice.
# application.yml
# OAuth2 Resource Server configuration
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// Gateway security configuration
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
// Disable CSRF for stateless API
.csrf(ServerHttpSecurity.CsrfSpec::disable)
// Authorization configuration
.authorizeExchange(exchanges -> exchanges
// Public endpoints
.pathMatchers("/actuator/health", "/actuator/info").permitAll()
.pathMatchers("/api/public/**").permitAll()
.pathMatchers("/api/auth/**").permitAll()
// Role-specific endpoints
.pathMatchers("/api/admin/**").hasRole("ADMIN")
.pathMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
// Everything else requires authentication
.anyExchange().authenticated()
)
// OAuth2 Resource Server with JWT
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
.build();
}
@Bean
public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
new JwtGrantedAuthoritiesConverter();
// Extract roles from "roles" claim
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
ReactiveJwtAuthenticationConverter converter =
new ReactiveJwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(
new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter)
);
return converter;
}
}// Token relay to downstream services
@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 -> {
// Relay token to downstream services
ServerHttpRequest request = exchange.getRequest()
.mutate()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt.getTokenValue())
// Add user information extracted from 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() {
// After authentication, before routing
return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() + 1;
}
}Ready to ace your Spring Boot interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Question 12: What are the best practices for monitoring and observability?
Gateway monitoring is crucial for identifying performance and availability issues in a microservices architecture.
# application.yml
# Observability configuration
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// Custom metrics filter
@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();
// Extract target service from route
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 for latency
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);
// Request counter
meterRegistry.counter(
"gateway.requests.total",
"route", routeId,
"status", statusCode
).increment();
// Log slow requests
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;
}
}// Tracing context propagation
@Component
@RequiredArgsConstructor
public class TracingFilter implements GlobalFilter, Ordered {
private final Tracer tracer;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Create or retrieve tracing span
Span span = tracer.nextSpan()
.name("gateway-request")
.tag("http.method", exchange.getRequest().getMethod().name())
.tag("http.url", exchange.getRequest().getURI().toString())
.start();
// Inject tracing headers
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;
}
}Essential metrics table:
| Metric | Description |
|-----------------------------------|----------------------------------|
| gateway.request.duration | Latency per route/status |
| gateway.requests.total | Request counter |
| resilience4j.circuitbreaker.state | Circuit breaker states |
| http.server.requests | Standard HTTP metrics |
| spring.cloud.gateway.routes | Active routes |Use Grafana with Spring Cloud Gateway and Resilience4j dashboards to visualize metrics. Configure alerts on P99 latency and error rate.
Conclusion
Spring Cloud Gateway is an essential component of modern microservices architectures. Key points to remember for interviews:
Architecture and concepts:
- ✅ Routes, Predicates, and Filters form the basic model
- ✅ Reactive architecture with WebFlux and Netty
- ✅ Native integration with Spring Cloud ecosystem
Essential features:
- ✅ Dynamic routing based on multiple criteria
- ✅ Pre/post filters for request/response transformation
- ✅ Load balancing with Spring Cloud LoadBalancer
Resilience and security:
- ✅ Circuit breaker with Resilience4j and fallback
- ✅ Retry with exponential backoff and jitter
- ✅ Centralized OAuth2/JWT authentication
Observability:
- ✅ Micrometer metrics with per-route tags
- ✅ Distributed tracing with context propagation
- ✅ Health checks and actuator endpoints
Mastering Spring Cloud Gateway demonstrates deep understanding of microservices patterns and scalability concerns. These skills are essential for designing robust and high-performance API Gateways.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

Spring Kafka: Event-Driven Architecture with Resilient Consumers
Complete Spring Kafka guide for event-driven architectures. Configuration, resilient consumers, retry policies, dead letter queues and production patterns for distributed applications.

Spring Boot Logging in 2026: Structured Logs for Production with Logback and JSON
Complete guide to Spring Boot structured logging. Logback JSON configuration, MDC for tracing, production best practices and ELK Stack integration.

Spring GraphQL Interview: Resolvers, DataLoaders and N+1 Problem Solutions
Prepare for Spring GraphQL interviews with this complete guide. Resolvers, DataLoaders, N+1 problem handling, mutations, and best practices for technical questions.