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/routesRoute 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 MBRate 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 1–20 → forwarded (burst-capacity=20)
// Requests 21–25 → 429 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 if ≥ 50% 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);
});
}
}