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: interview questions on routing, filters and load balancing

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.

Interview preparation tip

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.

GatewayApplication.javajava
// 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);
    }
}
yaml
# 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.

RouteConfiguration.javajava
// 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:

text
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 response

Question 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.

yaml
# 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
CustomPredicateFactory.javajava
// 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

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.

LoggingFilter.javajava
// 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;
    }
}
AuthenticationFilter.javajava
// 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.

yaml
# 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}"
RateLimiterConfiguration.javajava
// 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.

RequestBodyModificationFilter.javajava
// 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;
    }
}
ResponseBodyModificationConfig.javajava
// 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.

yaml
# 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/
LoadBalancerConfiguration.javajava
// 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);
    }
}
WeightedLoadBalancer.javajava
// 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.

yaml
# 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
FallbackController.javajava
// 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));
    }
}
CircuitBreakerEventListener.javajava
// 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());
    }
}
Timeout and circuit breaker

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.

yaml
# 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
CustomRetryFilter.javajava
// 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.

AggregationController.javajava
// 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());
    }
}
DashboardResponse.javajava
// 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.

yaml
# 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
SecurityConfig.javajava
// 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;
    }
}
TokenRelayFilter.javajava
// 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.

yaml
# 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
MetricsFilter.javajava
// 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;
    }
}
TracingFilter.javajava
// 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:

text
| 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                    |
Recommended dashboards

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

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

Share

Related articles