Spring Cloud Gateway — Interview Questions
Stack context: This system uses Spring Cloud Gateway as the single entry point (port 8080) for all six services. It runs on Spring WebFlux (Netty-based reactive), handles JWT session validation via Redis, applies per-user token-bucket rate limiting, and routes requests to downstream services. A local Caffeine cache (60 s, 10k entries) provides Redis fallback.
Q1 — What is Spring Cloud Gateway and what problem does it solve? junior
Answer: Spring Cloud Gateway (SCG) is a reactive API gateway built on Spring WebFlux and Project Reactor. It acts as the single entry point for all client requests in a microservices architecture, providing:
- Routing: Direct requests to the correct backend service based on path/headers.
- Cross-cutting concerns: Authentication, authorization, rate limiting, CORS, logging — handled once instead of in every service.
- Protocol translation: Transform request/response headers, strip prefixes, rewrite paths.
- Load balancing: Integrate with Spring Cloud LoadBalancer or Eureka.
Why not use NGINX?: SCG is Java-native and integrates directly with Spring Security, Spring Cloud, and custom WebFilter beans — enabling business logic (like reading Redis sessions) in the gateway without a separate process.
This system: API Gateway (port 8080) routes to user-service (8081), product-service (8082), and order-service (8083). It validates JWT sessions from Redis before forwarding requests.
Q2 — What is Spring WebFlux and how does it differ from Spring MVC? junior
Answer:
| Spring MVC | Spring WebFlux | |
|---|---|---|
| Threading model | One thread per request (Tomcat) | Event loop (Netty), few threads handle many requests |
| Programming model | Blocking, imperative | Non-blocking, reactive (Mono<T>, Flux<T>) |
| Backpressure | No built-in | Yes — via Project Reactor |
| I/O | Blocking JDBC, blocking HTTP | Non-blocking WebClient, reactive Redis |
| Memory efficiency | Each thread ~1 MB stack | ~100 bytes per async operation |
| Learning curve | Simple | Higher (reactive programming) |
When to use WebFlux: High concurrency with many concurrent I/O-bound requests (the gateway validates Redis on every request). WebFlux reuses a small thread pool for all concurrency.
When to use MVC: Business logic services with complex domain operations, blocking DB calls — simpler code.
This system: Gateway uses WebFlux (required for SCG). All other services use MVC.
Q3 — What is a GatewayFilter vs a GlobalFilter? junior
Answer: Both intercept requests/responses, but their scope differs:
GlobalFilter: Applied to ALL routes automatically. Use for cross-cutting concerns that apply to every request (logging, authentication, tracing).
GatewayFilter: Applied only to specific routes. Use for route-specific transformations (strip prefix, add headers, rewrite path).
// GlobalFilter — applies to all routes
@Component
public class SessionValidationFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = extractToken(exchange);
return validateSession(token)
.flatMap(session -> chain.filter(injectHeaders(exchange, session)));
}
@Override
public int getOrder() { return -1; } // run early (lower = earlier)
}
Built-in GatewayFilters (configured in YAML):
StripPrefix: Remove N path prefix segments.RewritePath: Regex path rewriting.AddRequestHeader: Inject headers before forwarding.RequestRateLimiter: Token-bucket rate limiting.CircuitBreaker: Resilience4j integration.
Q4 — How do you configure routes in Spring Cloud Gateway? junior
Answer:
Routes are configured in application.yml or programmatically with RouteLocatorBuilder.
YAML configuration:
spring:
cloud:
gateway:
routes:
- id: order-service
uri: http://order-service:8083
predicates:
- Path=/order-service/**
filters:
- StripPrefix=1 # remove /order-service prefix
- AddRequestHeader=X-Gateway-Version, 1.0
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"
Programmatic (Java):
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("product-service", r -> r
.path("/product-service/**")
.filters(f -> f.stripPrefix(1))
.uri("http://product-service:8082"))
.build();
}
This system: All routes defined in application.yml with StripPrefix=1 to remove the service prefix before forwarding.
Q5 — What are route predicates and what built-in predicates does SCG provide? junior
Answer: Predicates are conditions that must match for a route to be selected. Multiple predicates are AND-ed.
Built-in predicates:
| Predicate | Example | Match condition |
|---|---|---|
Path |
Path=/api/** |
Request path matches pattern |
Method |
Method=GET,POST |
HTTP method |
Header |
Header=X-Request-Id, \d+ |
Header exists and matches regex |
Query |
Query=color, red |
Query parameter exists/matches |
Host |
Host=**.example.com |
Host header matches pattern |
After |
After=2026-01-01T00:00:00Z |
Current time is after date |
RemoteAddr |
RemoteAddr=192.168.0.0/24 |
Client IP in CIDR range |
Weight |
Weight=serviceA, 80 |
Weighted routing (A/B, canary) |
Custom predicate: Implement RoutePredicateFactory<C> where C is a config class.
This system: Uses Path predicates exclusively. A Weight predicate would enable canary deployments.
Q6 — How does the RequestRateLimiter filter work in Spring Cloud Gateway? junior
Answer:
The RequestRateLimiter filter applies a token-bucket rate limiter per key (user, IP, etc.) using Redis for distributed state.
Configuration:
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # tokens added per second
redis-rate-limiter.burstCapacity: 20 # max tokens (burst allowance)
redis-rate-limiter.requestedTokens: 1 # tokens consumed per request
key-resolver: "#{@userKeyResolver}" # Bean that extracts the rate limit key
Key resolver (per-user rate limiting):
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.justOrEmpty(
exchange.getRequest().getHeaders().getFirst("X-User-Id")
).defaultIfEmpty("anonymous");
}
Response headers when rate limited:
X-RateLimit-Remaining: 5— tokens leftX-RateLimit-Burst-Capacity: 20- HTTP 429 when remaining = 0
This system: Rate limits by X-User-Id header injected by SessionValidationFilter before rate limiting runs.
Q7 — What is the reactive programming model in Project Reactor? junior
Answer: Project Reactor is the reactive library used by Spring WebFlux. It implements the Reactive Streams specification with two core types:
Mono<T>: Emits 0 or 1 item. Used for single async results.Flux<T>: Emits 0 to N items. Used for streams or collections.
Key operators:
// Transform value
Mono<String> upper = redis.get("key").map(String::toUpperCase);
// Chain async operations
Mono<Order> order = userService.getUser(id)
.flatMap(user -> orderService.createOrder(user, request)); // flatMap for async
// Error handling
Mono<Product> product = redis.get("product:" + id)
.switchIfEmpty(Mono.error(new NotFoundException("Product not found")));
// Fallback
Mono<Session> session = redis.get("session:" + token)
.switchIfEmpty(caffeine.get(token)); // fallback to local cache
Subscription model: Reactive pipelines are lazy — nothing executes until subscribed. subscribe(), block(), or returning a Mono/Flux from a WebFlux controller all trigger execution.
Q8 — How does ServerWebExchange work in Spring Cloud Gateway filters? junior
Answer:
ServerWebExchange is the reactive equivalent of HttpServletRequest + HttpServletResponse. It holds the current request/response and allows modification via immutable builders.
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Read request
ServerHttpRequest request = exchange.getRequest();
String token = request.getHeaders().getFirst("Authorization");
String path = request.getPath().value();
// Mutate request (add headers to forward)
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Id", session.getUserId())
.header("X-User-Role", session.getRole())
.build();
// Mutate response (add response headers)
exchange.getResponse().getHeaders().add("X-Gateway-Request-Id", UUID.randomUUID().toString());
// Forward with mutated request
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
Immutability: Request/response objects are immutable. All modifications create new instances via .mutate().build().
Q9 — What is Ordered interface and why does filter order matter? junior
Answer:
Ordered interface sets the execution order of filters. Lower value = higher priority = runs earlier.
Why order matters:
SessionValidationFilter(order -1) must run beforeRequestRateLimiter(order 1) — the rate limiter needsX-User-Idheader which session validation injects.- Logging filter (order
Integer.MIN_VALUE) must run first to capture the full request. - Error handling filter must run last (or first with proper wrapping) to catch downstream errors.
@Component
@Order(-1) // or implement Ordered
public class SessionValidationFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return -1;
}
}
Built-in filter orders: NettyRoutingFilter (order Integer.MAX_VALUE) runs last — it's the actual HTTP routing step. Custom filters should use negative values to run before routing.
Q10 — How do you handle circuit breaker pattern in Spring Cloud Gateway? senior
Answer:
SCG integrates with Resilience4j for circuit breaker functionality via the CircuitBreaker filter.
How it works:
- Circuit breaker monitors failure rate of requests to a downstream service.
- When failure rate exceeds threshold, the circuit "opens" — subsequent requests immediately fail (fast-fail) without calling the downstream service.
- After a
waitDurationInOpenState, the circuit enters "half-open" — a limited number of test requests are allowed through. - If they succeed, the circuit closes; if they fail, it reopens.
Configuration:
spring:
cloud:
gateway:
routes:
- id: order-service
uri: http://order-service:8083
filters:
- name: CircuitBreaker
args:
name: orderServiceCB
fallbackUri: forward:/fallback/order-service
Fallback endpoint:
@RestController
public class FallbackController {
@GetMapping("/fallback/order-service")
public Mono<ResponseEntity<ErrorResponse>> orderServiceFallback() {
return Mono.just(ResponseEntity.status(503)
.body(new ErrorResponse("SERVICE_UNAVAILABLE", "Order service is temporarily unavailable")));
}
}
Q11 — How does WebClient differ from RestTemplate in a reactive context? senior
Answer:
RestTemplate |
WebClient |
|
|---|---|---|
| Threading | Blocking — holds thread while waiting | Non-blocking — releases thread during I/O |
| Programming model | Synchronous | Reactive (Mono<T>, Flux<T>) |
| WebFlux compatibility | Blocks event loop if used in WebFlux | Native — works correctly |
| HTTP/2 | No | Yes |
| Streaming | Limited | Full streaming support |
Using WebClient in a Gateway filter:
@Autowired
private WebClient.Builder webClientBuilder;
public Mono<UserInfo> getUserInfo(String userId) {
return webClientBuilder.build()
.get()
.uri("http://user-service:8081/internal/users/{id}", userId)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
resp -> Mono.error(new UserNotFoundException(userId)))
.bodyToMono(UserInfo.class)
.timeout(Duration.ofSeconds(2));
}
Never use RestTemplate in a WebFlux application — it blocks the Netty event loop thread, causing thread starvation under load.
Q12 — What is path rewriting and how do you use RewritePath filter? junior
Answer:
RewritePath uses regex substitution to transform the request path before forwarding to the upstream service.
Example: External path /api/v1/orders/{id} → internal path /orders/{id}
routes:
- id: order-service-v1
uri: http://order-service:8083
predicates:
- Path=/api/v1/orders/**
filters:
- RewritePath=/api/v1/orders/(?<segment>.*), /orders/${segment}
StripPrefix is simpler for prefix removal:
filters:
- StripPrefix=2 # removes /api/v1 from /api/v1/orders/123 → /orders/123
Use cases:
- API versioning: Expose
/api/v2/productsexternally, route to existing/productsinternally. - Legacy path migration: Old clients use old paths; service has refactored URLs.
- Path normalization: Normalize case or add/remove trailing slashes.
Q13 — How do you implement CORS in Spring Cloud Gateway? junior
Answer: CORS (Cross-Origin Resource Sharing) must be configured at the gateway level. If individual services also configure CORS, duplicate headers can cause browser errors.
Gateway-level CORS:
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins:
- "https://app.example.com"
- "http://localhost:3000"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowedHeaders: "*"
allowCredentials: true
maxAge: 3600
Programmatic (more control):
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("https://*.example.com"));
config.setAllowedMethods(List.of("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
Important: Disable CORS config in individual microservices to prevent duplicate Access-Control-Allow-Origin headers.
Q14 — What is the difference between pre-filter and post-filter in SCG? senior
Answer: Filters can execute logic before the request is routed (pre-filter) and/or after the response is received (post-filter).
@Component
public class RequestResponseLoggingFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// PRE-FILTER: runs before forwarding
log.info("Request: {} {}", exchange.getRequest().getMethod(), exchange.getRequest().getPath());
long startTime = System.currentTimeMillis();
return chain.filter(exchange)
// POST-FILTER: runs after response received — flatMap on Mono completion
.then(Mono.fromRunnable(() -> {
long duration = System.currentTimeMillis() - startTime;
log.info("Response: {} in {} ms",
exchange.getResponse().getStatusCode(), duration);
}));
}
}
Use cases:
- Pre-filter: Authentication, rate limiting, request transformation, request ID injection.
- Post-filter: Response header modification, response logging, metrics recording, response body transformation (decryption, compression).
Q15 — How would you implement request body logging in Spring Cloud Gateway without consuming the body? senior
Answer:
In WebFlux, the request body is a Flux<DataBuffer> that can only be consumed once. To log it while still forwarding it, you must buffer and re-publish.
@Component
public class RequestBodyLoggingFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
String body = new String(bytes, StandardCharsets.UTF_8);
log.info("Request body: {}", body); // CAUTION: mask sensitive fields!
// Re-wrap bytes into new request
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return Flux.just(exchange.getResponse().bufferFactory().wrap(bytes));
}
};
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
}
Warning: Logging request bodies can expose PII (passwords, card numbers). Always mask sensitive fields before logging. Consider whether logging at the gateway level is necessary or if service-level logging is more appropriate.
Q16 — How does load balancing work in Spring Cloud Gateway? senior
Answer:
SCG integrates with Spring Cloud LoadBalancer for client-side load balancing. Instead of a static uri: http://order-service:8083, use the lb:// scheme:
routes:
- id: order-service
uri: lb://order-service # service ID registered in discovery
predicates:
- Path=/order-service/**
Service discovery integration:
- Eureka: Services register with Eureka; SCG queries Eureka for instances of
order-service. - Kubernetes: Uses KubernetesDiscoveryClient to discover pods via K8s DNS.
- Static list (no registry):
spring.cloud.loadbalancer.instances.order-service[0].uri=http://order-service-1:8083.
Load balancing algorithms:
RoundRobinLoadBalancer(default): Each request goes to the next available instance.RandomLoadBalancer: Random selection.- Custom: Implement
ReactorServiceInstanceLoadBalancer.
This system: Uses static URIs (single instance per service in Docker Compose). lb:// would be used in a Kubernetes or Eureka-based deployment.
Q17 — How do you test a Spring Cloud Gateway route configuration? senior
Answer: Testing strategies from simplest to most comprehensive:
1. Unit test the filter logic with MockServerWebExchange:
@Test
void sessionFilterRejectsUnauthenticatedRequest() {
MockServerWebExchange exchange = MockServerWebExchange.from(
MockServerHttpRequest.get("/order-service/orders").build());
GatewayFilterChain chain = e -> Mono.empty();
sessionFilter.filter(exchange, chain).block();
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
2. Integration test with @WebFluxTest + WireMock:
@SpringBootTest(webEnvironment = RANDOM_PORT)
class GatewayRoutingTest {
@Autowired WebTestClient webTestClient;
// WireMock stubs the downstream service
@Test
void routesOrderRequestToOrderService() {
wiremock.stubFor(get("/orders/123").willReturn(ok().withBody("{\"id\":\"123\"}")));
webTestClient.get().uri("/order-service/orders/123")
.header("Authorization", "Bearer " + validToken)
.exchange()
.expectStatus().isOk();
}
}
This system: Uses WireMock (wiremock-spring-boot) to stub all downstream services in gateway integration tests, avoiding the need to start all 6 services.
Q18 — What is header-based routing and how would you implement API versioning with it? senior
Answer: Header-based routing selects routes based on HTTP request headers, enabling API versioning without URL changes.
routes:
# V2 API — newer clients send Accept-Version: v2
- id: product-service-v2
uri: http://product-service-v2:8092
predicates:
- Path=/products/**
- Header=Accept-Version, v2
order: 1 # lower order = higher priority
# V1 API — default for older clients
- id: product-service-v1
uri: http://product-service:8082
predicates:
- Path=/products/**
order: 2
Versioning strategies:
- Header (
Accept-Version: v2): Clean URLs, but less discoverable. - URL path (
/api/v2/products): Clear and discoverable; requires URL changes for new versions. - Content-Type (
Accept: application/vnd.ecommerce.v2+json): REST-pure but complex.
Canary with Weight predicate: Route 10% of traffic to the new version for gradual rollout:
predicates:
- Weight=product-service-group, 10 # 10% to V2
Q19 — How do you handle timeout and retry in Spring Cloud Gateway? senior
Answer: Global timeouts:
spring:
cloud:
gateway:
httpclient:
connect-timeout: 2000 # 2s to establish TCP connection
response-timeout: 10s # 10s to receive full response
Per-route timeouts (via Metadata filter):
routes:
- id: slow-report-service
uri: http://report-service:8090
metadata:
connect-timeout: 5000
response-timeout: 60000 # reports can take up to 60s
Retry filter:
filters:
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT
methods: GET # only retry idempotent methods
backoff:
firstBackoff: 100ms
maxBackoff: 2000ms
factor: 2
Caution: Never retry POST, PUT, DELETE unless the downstream service is idempotent. Retrying a non-idempotent request (e.g., place order) causes duplicate side effects.
Q20 — How would you implement request tracing and correlation IDs in the gateway? senior
Answer: A correlation ID allows tracing a request across all services in logs without Zipkin.
Implementation:
@Component
@Order(Integer.MIN_VALUE) // run first
public class CorrelationIdFilter implements GlobalFilter {
private static final String CORRELATION_HEADER = "X-Correlation-Id";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String correlationId = exchange.getRequest().getHeaders()
.getFirst(CORRELATION_HEADER);
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
final String finalId = correlationId;
// Add to MDC for gateway logs
return chain.filter(
exchange.mutate()
.request(exchange.getRequest().mutate()
.header(CORRELATION_HEADER, finalId).build())
.response(new ServerHttpResponseDecorator(exchange.getResponse()) {
@Override
public HttpHeaders getHeaders() {
super.getHeaders().add(CORRELATION_HEADER, finalId);
return super.getHeaders();
}
})
.build()
);
}
}
Downstream services read X-Correlation-Id and include it in all log statements. Combined with Zipkin's traceId, this provides both human-readable and machine-readable trace correlation.
Q21 — What is the ModifyRequestBody filter and when is it needed? senior
Answer:
ModifyRequestBody allows transforming the request body before it reaches the downstream service. It's a built-in GatewayFilter factory.
Use cases:
- Adding authentication tokens to the body (legacy APIs that expect auth in body).
- Body format conversion (XML → JSON).
- Injecting tenant context into the body.
- Body sanitization/validation at the gateway.
@Bean
public RouteLocatorBuilder.Builder routeWithBodyModification(RouteLocatorBuilder builder) {
return builder.routes()
.route("order-service", r -> r
.path("/order-service/**")
.filters(f -> f.modifyRequestBody(
String.class, String.class,
MediaType.APPLICATION_JSON_VALUE,
(exchange, body) -> {
// Inject X-User-Id from header into JSON body
JsonNode node = objectMapper.readTree(body);
((ObjectNode) node).put("userId", exchange.getRequest().getHeaders().getFirst("X-User-Id"));
return Mono.just(objectMapper.writeValueAsString(node));
}
))
.uri("http://order-service:8083")
);
}
Performance note: Body modification requires buffering the entire request body in memory. Avoid for large payloads; prefer injecting context via headers (which this system does).
Q22 — How do you secure actuator endpoints in Spring Cloud Gateway? senior
Answer: Actuator endpoints expose sensitive operational data. Secure them from external access:
1. Separate management port (expose only internally):
management:
server:
port: 8090 # different from 8080 (external)
endpoints:
web:
exposure:
include: "health,info,prometheus"
Network policy: block port 8090 from external internet; allow only monitoring systems.
2. Path restriction via gateway predicates:
routes:
- id: actuator-block
uri: no://op
predicates:
- Path=/*/actuator/**
filters:
- name: SetStatus
args:
status: 403
order: -10 # before other routes
3. Spring Security on the gateway itself:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/**").hasRole("OPS")
.anyRequest().authenticated()
);
This system: Actuator endpoints are internal to the Docker network. Gateway blocks external access to /actuator/** paths.
Q23 — What is the DedupeResponseHeader filter? junior
Answer:
DedupeResponseHeader removes duplicate headers from the response. This is commonly needed when both the gateway and downstream services add the same header (e.g., CORS headers).
filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST
Strategies:
RETAIN_FIRST(default): Keep the first occurrence.RETAIN_LAST: Keep the last occurrence.RETAIN_UNIQUE: Keep all unique values.
When needed: When the gateway adds CORS headers AND the downstream service also adds them, browsers reject requests due to duplicate Access-Control-Allow-Origin headers. DedupeResponseHeader resolves this without removing CORS config from services.
Q24 — How do you implement a custom KeyResolver for rate limiting? junior
Answer:
KeyResolver extracts the rate limit key from the ServerWebExchange. The key determines what is rate-limited (per user, per IP, per API key, etc.).
// Rate limit by user ID (authenticated requests)
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.justOrEmpty(
exchange.getRequest().getHeaders().getFirst("X-User-Id")
).defaultIfEmpty("anonymous");
}
// Rate limit by IP address
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
Objects.requireNonNull(exchange.getRequest().getRemoteAddress())
.getAddress().getHostAddress()
);
}
// Rate limit by API key from query parameter
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> Mono.justOrEmpty(
exchange.getRequest().getQueryParams().getFirst("api_key")
).switchIfEmpty(Mono.error(new MissingApiKeyException()));
}
This system: Uses userKeyResolver() — authenticated users are rate limited by their user ID. Anonymous requests (if allowed) fall under the "anonymous" bucket (shared rate limit).
Q25 — What is the difference between using WebTestClient and MockMvc for testing WebFlux apps? junior
Answer:
MockMvc |
WebTestClient |
|
|---|---|---|
| Runtime | Simulates the servlet layer (no server) | Full reactive client (can test against real port) |
| Reactive support | No | Yes — handles Mono/Flux responses |
| Use with | Spring MVC (@WebMvcTest) |
Spring WebFlux (@WebFluxTest or @SpringBootTest) |
| Real HTTP | No (mock dispatch) | Optional — can bind to mock or real server |
WebTestClient for Gateway tests:
@SpringBootTest(webEnvironment = RANDOM_PORT)
class GatewayTest {
@Autowired
WebTestClient webTestClient;
@Test
void rateLimiterReturns429() {
for (int i = 0; i < 25; i++) {
webTestClient.get().uri("/products/123")
.header("X-User-Id", "user-1")
.exchange();
}
webTestClient.get().uri("/products/123")
.header("X-User-Id", "user-1")
.exchange()
.expectStatus().isEqualTo(429);
}
}
Q26 — How does Spring Cloud Gateway handle WebSocket proxying? senior
Answer: SCG supports WebSocket proxying transparently. WebSocket connections are established via an HTTP Upgrade handshake; SCG proxies the upgrade and then streams the bidirectional WebSocket frames.
Configuration (same as HTTP — SCG detects WebSocket via Upgrade header):
routes:
- id: websocket-service
uri: ws://chat-service:8086 # or wss:// for SSL
predicates:
- Path=/ws/**
Key considerations:
- Long-lived WebSocket connections hold resources on the gateway's event loop.
- Rate limiting applies to the initial HTTP connection, not individual messages.
- Load balancing: WebSocket connections are sticky (all messages on one connection go to the same backend instance). Use session affinity if load balancing WebSocket servers.
- Filters work on the initial HTTP request (pre-upgrade). They cannot inspect individual WebSocket frames.
Q27 — What is sticky session routing and when is it needed in SCG? senior
Answer: Sticky session (session affinity) routes repeated requests from the same client to the same backend instance. Needed when a backend service maintains in-memory state per client.
SCG implementation with Cookie predicate + lb://:
routes:
- id: stateful-service
uri: lb://stateful-service
predicates:
- Cookie=INSTANCE_ID, .* # route based on cookie
filters:
- name: LoadBalancerClientFilter
args:
# configures sticky sessions via StickySessionLoadBalancer
Or with StickySessionLoadBalancer:
@Bean
public ReactorLoadBalancer<ServiceInstance> stickySessionLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new StickySessionLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
Better practice: Design backend services to be stateless. Store state in Redis (as this system does for sessions) so any instance can handle any request.
Q28 — How do you debug routing issues in Spring Cloud Gateway? junior
Answer: Step-by-step debugging approach:
1. Enable route logging:
logging:
level:
org.springframework.cloud.gateway: TRACE
reactor.netty: DEBUG
2. Check /actuator/gateway/routes (requires actuator exposure):
management.endpoints.web.exposure.include: "gateway"
GET /actuator/gateway/routes lists all configured routes with predicates and filters.
3. /actuator/gateway/globalfilters: Lists all global filters in execution order.
4. X-Forwarded-* headers: Inspect what headers the gateway adds to forwarded requests.
5. Common issues:
- Route not matching: Check predicate path — is trailing
/needed? IsStripPrefixcount correct? - 503 on valid route: Backend service is down or connection refused. Check Docker network.
- 401 on valid session: Session filter running before token header is set, or Redis is down (Caffeine miss).
- Duplicate CORS headers: Both gateway and service adding CORS headers → use
DedupeResponseHeader.
Q29 — How would you implement a gateway plugin for request/response body encryption? senior
Answer: End-to-end body encryption at the gateway decouples business services from encryption concerns.
Architecture:
- Client sends AES-encrypted body with
Content-Encoding: encrypted. - Gateway
DecryptRequestBodyFilterdecrypts and forwards plaintext to service. - Service responds with plaintext.
- Gateway
EncryptResponseBodyFilterencrypts and returns to client.
@Component
public class DecryptRequestBodyFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (!isEncrypted(exchange)) return chain.filter(exchange);
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(buf -> {
byte[] encrypted = readBytes(buf);
byte[] decrypted = aesDecrypt(encrypted, getKeyForRequest(exchange));
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(decrypted.length))
.build();
return chain.filter(exchange.mutate()
.request(new BodyInjectingRequest(mutated, decrypted)).build());
});
}
}
Security considerations: Key management is critical — use a KMS (AWS KMS, Azure Key Vault), not hardcoded keys. Rotate keys regularly. Log decryption failures for security monitoring.
Q30 — What are the production considerations for running Spring Cloud Gateway at scale? senior
Answer: Key production concerns for a high-traffic gateway:
1. Tuning Netty thread pool:
spring:
cloud:
gateway:
httpclient:
pool:
max-connections: 1000 # per route
acquire-timeout: 5000ms
2. Memory tuning: Gateway buffers request/response bodies for filters. Large payloads require higher heap. Set -Xmx512m and monitor GC pressure.
3. Redis connection pool: Rate limiter makes Redis calls on every request. Tune Lettuce pool (max-active: 20) for high throughput.
4. Health checks: Use /actuator/health for load balancer probes. Set readiness (routes configured + Redis connected) separate from liveness (process alive).
5. Horizontal scaling: Gateway is stateless (sessions are in Redis). Scale to N replicas behind a load balancer without sticky sessions.
6. Circuit breakers on all routes: Prevent a slow downstream service from cascading and blocking all gateway threads.
7. Observability: Export Micrometer metrics to Prometheus. Key metrics: gateway.requests (by route, status), gateway.requests.latency, Redis rate limiter latency.
8. Graceful shutdown: Configure server.shutdown=graceful and spring.lifecycle.timeout-per-shutdown-phase=30s to drain in-flight requests before shutdown.