Spring BootSpring Cloud Gateway
Spring Boot

Spring Cloud Gateway

Spring Cloud Gateway (SCG) is the official API gateway implementation for Spring Boot microservices. Built on Spring WebFlux and Project Reactor, it is fully non-blocking and reactive. It routes requests to downstream services using predicates (matching rules) and applies filters (pre/post processing) such as path rewriting, header manipulation, rate limiting, and circuit breaking.

Setup and Dependencies

Spring Cloud Gateway replaces the older Netflix Zuul. It requires the reactive stack (spring-boot-starter-webflux) — do not include spring-boot-starter-web alongside it, as they conflict. The Eureka client starter is added so the gateway can resolve lb:// URIs via service discovery.
XML
<!-- pom.xml: -->
<dependencies>

    <!-- Spring Cloud Gateway (includes WebFlux — do NOT add spring-boot-starter-web) -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <!-- Service discovery — resolves lb://service-name via Eureka: -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <!-- Circuit breaker for gateway fallback routes: -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
    </dependency>

    <!-- Rate limiting (uses Redis as token-bucket store): -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>

    <!-- Actuator — exposes /actuator/gateway/routes for inspection: -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2023.0.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Route Configuration (YAML)

A route is the core building block of Spring Cloud Gateway. Each route has an id, a uri (where to forward), a list of predicates (when to match), and a list of filters (what to do before/after forwarding). Routes are evaluated in order; the first matching route wins.
yaml
# application.yml
server:
  port: 8080

spring:
  application:
    name: api-gateway

  cloud:
    gateway:
      # ── Global default filters (applied to every route): ─────────────
      default-filters:
        - AddRequestHeader=X-Gateway-Source, api-gateway
        - AddRequestHeader=X-Request-Id, ${random.uuid}
        - DedupeResponseHeader=Access-Control-Allow-Origin

      routes:

        # ── Route 1: User Service ──────────────────────────────────────
        - id: user-service
          uri: lb://user-service          # lb:// = load-balanced via Eureka
          predicates:
            - Path=/api/users/**          # match this path pattern
          filters:
            - StripPrefix=1               # strip /api before forwarding
            # /api/users/1 → forwarded as /users/1 to user-service

        # ── Route 2: Order Service ─────────────────────────────────────
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=1

        # ── Route 3: Header-based routing (canary) ─────────────────────
        - id: user-service-canary
          uri: lb://user-service-v2
          predicates:
            - Path=/api/users/**
            - Header=X-Canary, true       # only if header X-Canary: true
          filters:
            - StripPrefix=1

        # ── Route 4: Method predicate ──────────────────────────────────
        - id: order-service-readonly
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
            - Method=GET,HEAD             # only allow read-only methods

        # ── Route 5: Static fallback for unknown routes ────────────────
        - id: fallback-route
          uri: no://op
          predicates:
            - Path=/**
          filters:
            - SetStatus=404

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

management:
  endpoints:
    web:
      exposure:
        include: gateway, health, info
  endpoint:
    gateway:
      enabled: true   # exposes /actuator/gateway/routes

Route Configuration (Java DSL)

Routes can be defined programmatically using the RouteLocatorBuilder Java DSL. This is useful when route logic depends on runtime conditions, Spring beans, or environment variables that are not easily expressed in YAML.
Java
@Configuration
public class GatewayRoutesConfig {

    // ── Basic routes mirroring the YAML above: ────────────────────────
    @Bean
    public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
        return builder.routes()

            // User Service route:
            .route("user-service", r -> r
                .path("/api/users/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Gateway-Source", "api-gateway")
                    .addRequestHeader("X-Request-Id",
                        UUID.randomUUID().toString()))
                .uri("lb://user-service"))

            // Order Service route:
            .route("order-service", r -> r
                .path("/api/orders/**")
                .filters(f -> f.stripPrefix(1))
                .uri("lb://order-service"))

            // Canary route — header predicate evaluated first:
            .route("user-service-canary", r -> r
                .path("/api/users/**")
                .and()
                .header("X-Canary", "true")
                .filters(f -> f.stripPrefix(1))
                .uri("lb://user-service-v2"))

            // Path rewrite — /api/v1/products/** → /products/**:
            .route("product-service", r -> r
                .path("/api/v1/products/**")
                .filters(f -> f
                    .rewritePath(
                        "/api/v1/(?<segment>.*)",
                        "/${segment}"))   // capture group rewrite
                .uri("lb://product-service"))

            .build();
    }
}

Built-in Filters

Spring Cloud Gateway ships with a large library of built-in GatewayFilter factories. Filters run in two phases: pre-filters execute before the request is forwarded; post-filters execute after the response is received. The most commonly used built-in filters cover path manipulation, header management, redirects, and retries.
yaml
// ── Path filters: ─────────────────────────────────────────────────────
// StripPrefix=1         → /api/users/1  becomes  /users/1
// PrefixPath=/v2        → /users/1      becomes  /v2/users/1
// RewritePath=/api/(?<seg>.*), /${seg}
//                       → /api/orders/5 becomes  /orders/5
// SetPath=/fixed/path   → replaces the entire path

// ── Header filters: ───────────────────────────────────────────────────
// AddRequestHeader=X-Source, gateway
// AddResponseHeader=X-Response-Time, ${responseTime}
// RemoveRequestHeader=Cookie
// RemoveResponseHeader=X-Internal-Error-Detail
// SetRequestHeader=X-User-Id, ${claim.sub}   (with token relay)
// MapRequestHeader=X-Custom-In, X-Custom-Out  (rename a header)

// ── Redirect / status filters: ────────────────────────────────────────
// RedirectTo=301, https://new-domain.com
// SetStatus=202
// SetResponseHeader=Content-Type, application/json

// ── Retry filter: ─────────────────────────────────────────────────────
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - name: Retry
              args:
                retries: 3               # retry up to 3 times
                statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE
                methods: GET, HEAD       # only retry idempotent methods
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 1000ms
                  factor: 2             # exponential: 100ms, 200ms, 400ms

// ── Request size limiter: ─────────────────────────────────────────────
//            - name: RequestSize
//              args:
//                maxSize: 5MB          # reject bodies larger than 5 MB

Rate Limiting

Spring Cloud Gateway includes a Redis-backed RequestRateLimiter filter that implements the token-bucket algorithm. Tokens are replenished at a configurable rate. When a client exhausts its tokens the gateway returns 429 Too Many Requests immediately, before the request reaches any downstream service.
Java
// ── Redis must be running (rate limiter stores state in Redis): ───────
// application.yml:
// spring:
//   data:
//     redis:
//       host: localhost
//       port: 6379

// ── KeyResolver — determines the rate-limit bucket per client: ────────
@Configuration
public class RateLimiterConfig {

    // Rate-limit by authenticated user (JWT sub claim):
    @Bean
    @Primary
    public KeyResolver userKeyResolver() {
        return exchange -> exchange.getPrincipal()
            .map(Principal::getName)
            .defaultIfEmpty("anonymous");
    }

    // Alternative: rate-limit by client IP address:
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.justOrEmpty(
            exchange.getRequest().getRemoteAddress())
            .map(addr -> addr.getAddress().getHostAddress())
            .defaultIfEmpty("unknown");
    }
}

// ── Route with rate limiter filter: ──────────────────────────────────
// application.yml:
// spring:
//   cloud:
//     gateway:
//       routes:
//         - id: order-service
//           uri: lb://order-service
//           predicates:
//             - Path=/api/orders/**
//           filters:
//             - name: RequestRateLimiter
//               args:
//                 redis-rate-limiter:
//                   replenish-rate: 10      # tokens added per second
//                   burst-capacity: 20      # max tokens in bucket
//                   requested-tokens: 1     # tokens consumed per request
//                 key-resolver: "#{@userKeyResolver}"
//
// Example behaviour:
//   Client A sends 25 requests in 1 second:
//     Requests 120  → forwarded (burst-capacity=20)
//     Requests 2125429 Too Many Requests
//   After 1 second: 10 new tokens added (replenish-rate=10)

Circuit Breaker Filter

The CircuitBreaker filter wraps a route with a Resilience4j circuit breaker. If the downstream service starts failing or timing out, the circuit opens and subsequent requests immediately receive a fallback response — protecting the gateway's thread pool and giving the failing service time to recover.
Java
// application.yml — circuit breaker on a route: ─────────────────────
// spring:
//   cloud:
//     gateway:
//       routes:
//         - id: payment-service
//           uri: lb://payment-service
//           predicates:
//             - Path=/api/payments/**
//           filters:
//             - name: CircuitBreaker
//               args:
//                 name: paymentCircuitBreaker
//                 fallbackUri: forward:/fallback/payments
//                 # on circuit open → forward internally to fallback controller

// ── Resilience4j circuit breaker configuration: ───────────────────────
// resilience4j:
//   circuitbreaker:
//     instances:
//       paymentCircuitBreaker:
//         sliding-window-size: 10
//         failure-rate-threshold: 50       # open if50% of 10 calls fail
//         wait-duration-in-open-state: 10s
//         permitted-number-of-calls-in-half-open-state: 3
//   timelimiter:
//     instances:
//       paymentCircuitBreaker:
//         timeout-duration: 3s             # treat call as failure after 3s

// ── Fallback controller (runs inside the gateway process): ────────────
@RestController
@RequestMapping("/fallback")
public class FallbackController {

    @GetMapping("/payments")
    public ResponseEntity<Map<String, Object>> paymentFallback(
            ServerWebExchange exchange) {
        // Retrieve the exception that triggered the fallback:
        Throwable cause = exchange.getAttribute(
            ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR);

        return ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(Map.of(
                "status",  "unavailable",
                "message", "Payment service is temporarily unavailable. " +
                           "Please try again in a few moments.",
                "cause",   cause != null ? cause.getMessage() : "unknown"
            ));
    }

    @GetMapping("/orders")
    public ResponseEntity<Map<String, Object>> orderFallback() {
        return ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(Map.of(
                "status",  "unavailable",
                "message", "Order service is temporarily unavailable.",
                "orders",  Collections.emptyList()
            ));
    }
}

Custom Global Filter (Auth / Logging)

A GlobalFilter applies to every route without being declared per route. It is the correct place to implement cross-cutting concerns such as JWT validation, request ID propagation, and access logging. The filter chain is ordered — use @Order or implement Ordered to control execution position relative to built-in filters.
Java
// ── JWT Authentication Global Filter: ────────────────────────────────
@Component
@Order(1)   // run first — before routing filters
public class JwtAuthenticationFilter implements GlobalFilter {

    private final JwtTokenProvider jwtProvider;

    // Paths that bypass authentication:
    private static final List<String> PUBLIC_PATHS = List.of(
        "/api/auth/login",
        "/api/auth/register",
        "/actuator/health"
    );

    @Override
    public Mono<Void> filter(ServerWebExchange exchange,
                             GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();

        // Skip auth for public endpoints:
        if (PUBLIC_PATHS.stream().anyMatch(path::startsWith)) {
            return chain.filter(exchange);
        }

        // Extract Bearer token from Authorization header:
        String authHeader = request.getHeaders()
            .getFirst(HttpHeaders.AUTHORIZATION);

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            exchange.getResponse()
                .setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        String token = authHeader.substring(7);

        if (!jwtProvider.validateToken(token)) {
            exchange.getResponse()
                .setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        // Forward user identity to downstream services via headers:
        String userId   = jwtProvider.getUserId(token);
        String userRole = jwtProvider.getRole(token);

        ServerHttpRequest mutatedRequest = request.mutate()
            .header("X-User-Id",   userId)
            .header("X-User-Role", userRole)
            .header(HttpHeaders.AUTHORIZATION, "")  // strip token
            .build();

        return chain.filter(exchange.mutate()
            .request(mutatedRequest).build());
    }
}

// ── Request Logging Global Filter: ───────────────────────────────────
@Component
@Order(2)
@Slf4j
public class LoggingFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange,
                             GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        long startTime = System.currentTimeMillis();

        log.info("→ {} {} | IP: {} | RequestId: {}",
            request.getMethod(),
            request.getURI().getPath(),
            request.getRemoteAddress(),
            request.getHeaders().getFirst("X-Request-Id"));

        return chain.filter(exchange)
            .doFinally(signal -> {
                long duration = System.currentTimeMillis() - startTime;
                log.info("← {} {} | Status: {} | {}ms",
                    request.getMethod(),
                    request.getURI().getPath(),
                    exchange.getResponse().getStatusCode(),
                    duration);
            });
    }
}