
Introduction
When building microservices, routing requests correctly is one of the most important pieces of the architecture. Instead of calling services directly, applications rely on an API gateway to route traffic, apply rules, handle authentication, and enforce policies. Spring Cloud Gateway makes this process simple, fast, and reactive.
In this comprehensive guide, you’ll learn how Spring Cloud Gateway works, how to define routes with predicates and filters, implement rate limiting, add authentication, integrate with service discovery, and build custom filters for advanced use cases.
What Is Spring Cloud Gateway?
Spring Cloud Gateway is a lightweight, reactive API gateway built on top of Spring WebFlux and Project Reactor. It provides a simple way to route requests to downstream services while applying filters such as authentication, rate limiting, logging, and header manipulation. Because it uses a reactive engine, it offers strong performance even under heavy load.
Key Features
- Dynamic routing based on paths, headers, query parameters, and more
- Reactive and non-blocking architecture for high throughput
- Extensible filters for request/response modification
- Built-in rate limiting with Redis integration
- Circuit breaker support with Resilience4j
- Service discovery integration with Eureka, Consul, or Kubernetes
- Load balancing across service instances
Gateway Architecture
# Spring Cloud Gateway Request Flow
# ==================================
#
# Client Request
# ↓
# Gateway Handler Mapping (matches route predicates)
# ↓
# Pre-Filters (authentication, logging, rate limiting)
# ↓
# Proxy to Backend Service
# ↓
# Post-Filters (response modification, metrics)
# ↓
# Client Response
Project Setup
Dependencies
<!-- pom.xml -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>
<dependencies>
<!-- Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Service Discovery -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Rate Limiting with Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- Circuit Breaker -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
<!-- Security (OAuth2 Resource Server) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- Actuator for monitoring -->
<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>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Route Configuration
YAML Configuration
# application.yml
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
# Global CORS configuration
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowedHeaders: "*"
maxAge: 3600
# Default filters applied to all routes
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin
- AddResponseHeader=X-Gateway-Version, v1.0
routes:
# User Service Route
- id: user-service
uri: lb://user-service # Load balanced to Eureka service
predicates:
- Path=/api/users/**
- Method=GET,POST,PUT,DELETE
filters:
- StripPrefix=1 # Remove /api prefix
- AddRequestHeader=X-Source, gateway
- name: CircuitBreaker
args:
name: userServiceCB
fallbackUri: forward:/fallback/users
# Order Service Route
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"
# Product Service with Path Rewriting
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/v1/products/**
filters:
- RewritePath=/api/v1/products/(?<segment>.*), /products/${segment}
# Auth Service (public)
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/auth/**
filters:
- name: CircuitBreaker
args:
name: authServiceCB
fallbackUri: forward:/fallback/auth
# Websocket Support
- id: notifications-ws
uri: lb:ws://notification-service
predicates:
- Path=/ws/notifications/**
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# Rate Limiter Redis Configuration
spring.data.redis:
host: localhost
port: 6379
# Circuit Breaker Configuration
resilience4j:
circuitbreaker:
instances:
userServiceCB:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 3
authServiceCB:
slidingWindowSize: 5
failureRateThreshold: 50
waitDurationInOpenState: 5000
timelimiter:
instances:
userServiceCB:
timeoutDuration: 3s
Java Configuration
// config/GatewayConfig.java
package com.example.gateway.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import java.time.Duration;
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRoutes(RouteLocatorBuilder builder) {
return builder.routes()
// User Service with multiple predicates
.route("user-service", r -> r
.path("/api/users/**")
.and()
.method(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Source", "gateway")
.addResponseHeader("X-Response-Time", String.valueOf(System.currentTimeMillis()))
.circuitBreaker(config -> config
.setName("userServiceCB")
.setFallbackUri("forward:/fallback/users"))
.retry(config -> config
.setRetries(3)
.setStatuses(HttpStatus.SERVICE_UNAVAILABLE)
.setBackoff(Duration.ofMillis(100), Duration.ofSeconds(1), 2, true)))
.uri("lb://user-service"))
// Header-based routing (API versioning)
.route("product-v2", r -> r
.path("/api/products/**")
.and()
.header("X-API-Version", "v2")
.filters(f -> f.stripPrefix(1))
.uri("lb://product-service-v2"))
// Default product route (v1)
.route("product-v1", r -> r
.path("/api/products/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://product-service"))
// Query parameter routing
.route("search-service", r -> r
.path("/api/search")
.and()
.query("type", "products|users|orders")
.filters(f -> f.stripPrefix(1))
.uri("lb://search-service"))
// Weight-based routing (canary deployment)
.route("order-service-canary", r -> r
.path("/api/orders/**")
.and()
.weight("orders", 10) // 10% traffic
.filters(f -> f.stripPrefix(1))
.uri("lb://order-service-canary"))
.route("order-service-stable", r -> r
.path("/api/orders/**")
.and()
.weight("orders", 90) // 90% traffic
.filters(f -> f.stripPrefix(1))
.uri("lb://order-service"))
.build();
}
}
Rate Limiting
// config/RateLimiterConfig.java
package com.example.gateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
@Configuration
public class RateLimiterConfig {
/**
* Rate limit by user ID from JWT token
*/
@Bean
public KeyResolver userKeyResolver() {
return exchange -> {
// Extract user from JWT or use IP as fallback
String user = exchange.getRequest().getHeaders().getFirst("X-User-Id");
if (user != null) {
return Mono.just(user);
}
// Fallback to IP address
return Mono.just(
exchange.getRequest().getRemoteAddress() != null
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
: "anonymous"
);
};
}
/**
* Rate limit by IP address
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress() != null
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
: "unknown"
);
}
/**
* Rate limit by API key
*/
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> {
String apiKey = exchange.getRequest().getHeaders().getFirst("X-API-Key");
return Mono.just(apiKey != null ? apiKey : "no-api-key");
};
}
}
Custom Filters
Logging Filter
// filter/LoggingGlobalFilter.java
package com.example.gateway.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.UUID;
@Component
public class LoggingGlobalFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(LoggingGlobalFilter.class);
private static final String CORRELATION_ID = "X-Correlation-ID";
private static final String START_TIME = "startTime";
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Generate or extract correlation ID
String correlationId = exchange.getRequest().getHeaders().getFirst(CORRELATION_ID);
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
// Store start time for duration calculation
exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
// Add correlation ID to request headers
ServerHttpRequest request = exchange.getRequest().mutate()
.header(CORRELATION_ID, correlationId)
.build();
final String finalCorrelationId = correlationId;
log.info("Incoming request: {} {} - CorrelationID: {}",
request.getMethod(),
request.getURI().getPath(),
finalCorrelationId);
return chain.filter(exchange.mutate().request(request).build())
.then(Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(START_TIME);
long duration = startTime != null ? System.currentTimeMillis() - startTime : 0;
log.info("Completed request: {} {} - Status: {} - Duration: {}ms - CorrelationID: {}",
request.getMethod(),
request.getURI().getPath(),
exchange.getResponse().getStatusCode(),
duration,
finalCorrelationId);
}));
}
@Override
public int getOrder() {
return -100; // Run early in the filter chain
}
}
Authentication Filter
// filter/JwtAuthenticationFilter.java
package com.example.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory {
private final JwtValidator jwtValidator;
public JwtAuthenticationFilter(JwtValidator jwtValidator) {
super(Config.class);
this.jwtValidator = jwtValidator;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// Skip authentication for public paths
String path = exchange.getRequest().getURI().getPath();
if (isPublicPath(path, config.getPublicPaths())) {
return chain.filter(exchange);
}
// Extract Authorization header
String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return onError(exchange, "Missing or invalid Authorization header", HttpStatus.UNAUTHORIZED);
}
String token = authHeader.substring(7);
// Validate JWT
return jwtValidator.validateToken(token)
.flatMap(claims -> {
// Check required roles if configured
if (config.getRequiredRoles() != null && !config.getRequiredRoles().isEmpty()) {
List userRoles = claims.get("roles", List.class);
if (userRoles == null || !userRoles.stream().anyMatch(config.getRequiredRoles()::contains)) {
return onError(exchange, "Insufficient permissions", HttpStatus.FORBIDDEN);
}
}
// Add user info to headers for downstream services
ServerWebExchange mutatedExchange = exchange.mutate()
.request(r -> r
.header("X-User-Id", claims.getSubject())
.header("X-User-Email", claims.get("email", String.class))
.header("X-User-Roles", String.join(",", claims.get("roles", List.class))))
.build();
return chain.filter(mutatedExchange);
})
.onErrorResume(e -> onError(exchange, "Invalid token: " + e.getMessage(), HttpStatus.UNAUTHORIZED));
};
}
private boolean isPublicPath(String path, List publicPaths) {
if (publicPaths == null) return false;
return publicPaths.stream().anyMatch(path::startsWith);
}
private Mono onError(ServerWebExchange exchange, String message, HttpStatus status) {
exchange.getResponse().setStatusCode(status);
exchange.getResponse().getHeaders().add("X-Error-Message", message);
return exchange.getResponse().setComplete();
}
public static class Config {
private List publicPaths;
private List requiredRoles;
public List getPublicPaths() { return publicPaths; }
public void setPublicPaths(List publicPaths) { this.publicPaths = publicPaths; }
public List getRequiredRoles() { return requiredRoles; }
public void setRequiredRoles(List requiredRoles) { this.requiredRoles = requiredRoles; }
}
}
Request Body Modification Filter
// filter/RequestBodyModificationFilter.java
package com.example.gateway.filter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
public class AddMetadataFilter extends AbstractGatewayFilterFactory {
private final ModifyRequestBodyGatewayFilterFactory modifyRequestBodyFilter;
private final ObjectMapper objectMapper;
public AddMetadataFilter(
ModifyRequestBodyGatewayFilterFactory modifyRequestBodyFilter,
ObjectMapper objectMapper) {
super(Config.class);
this.modifyRequestBodyFilter = modifyRequestBodyFilter;
this.objectMapper = objectMapper;
}
@Override
public GatewayFilter apply(Config config) {
return modifyRequestBodyFilter.apply(
new ModifyRequestBodyGatewayFilterFactory.Config()
.setRewriteFunction(JsonNode.class, JsonNode.class, (exchange, originalBody) -> {
if (originalBody == null) {
return Mono.empty();
}
// Add gateway metadata to request body
ObjectNode modifiedBody = originalBody.deepCopy();
ObjectNode metadata = modifiedBody.putObject("_gateway_metadata");
metadata.put("timestamp", System.currentTimeMillis());
metadata.put("gateway_version", "1.0");
metadata.put("source_ip",
exchange.getRequest().getRemoteAddress() != null
? exchange.getRequest().getRemoteAddress().getHostString()
: "unknown");
return Mono.just(modifiedBody);
})
);
}
public static class Config {
// Configuration options if needed
}
}
Fallback Controller
// controller/FallbackController.java
package com.example.gateway.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.Map;
@RestController
@RequestMapping("/fallback")
public class FallbackController {
@GetMapping("/users")
public ResponseEntity
Security Configuration
// config/SecurityConfig.java
package com.example.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.csrf(csrf -> csrf.disable())
.authorizeExchange(exchange -> exchange
// Public endpoints
.pathMatchers("/auth/**").permitAll()
.pathMatchers("/actuator/health").permitAll()
.pathMatchers("/fallback/**").permitAll()
// Protected endpoints
.pathMatchers("/api/admin/**").hasRole("ADMIN")
.pathMatchers("/api/**").authenticated()
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> {})
)
.build();
}
}
Common Mistakes to Avoid
1. Not Propagating Headers
# BAD: User context lost in downstream services
filters:
- StripPrefix=1
# GOOD: Propagate authentication headers
filters:
- StripPrefix=1
- PreserveHostHeader
- TokenRelay # For OAuth2
2. Missing Timeouts
# BAD: No timeouts, gateway can hang
spring:
cloud:
gateway:
routes:
- id: slow-service
uri: lb://slow-service
# GOOD: Always configure timeouts
spring:
cloud:
gateway:
httpclient:
connect-timeout: 2000
response-timeout: 5s
routes:
- id: slow-service
uri: lb://slow-service
metadata:
response-timeout: 10000
connect-timeout: 2000
3. Hardcoded URIs
# BAD: Hardcoded service URLs
uri: http://localhost:8081
# GOOD: Use service discovery
uri: lb://user-service
4. No Circuit Breaker
# BAD: Failing service brings down gateway
routes:
- id: user-service
uri: lb://user-service
# GOOD: Circuit breaker with fallback
routes:
- id: user-service
uri: lb://user-service
filters:
- name: CircuitBreaker
args:
name: userCB
fallbackUri: forward:/fallback/users
5. Rate Limiting Without Key Resolver
# BAD: Global rate limit affects all users equally
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
# GOOD: Per-user rate limiting
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"
Monitoring and Actuator
# Enable gateway actuator endpoints
management:
endpoints:
web:
exposure:
include: health,info,metrics,gateway
endpoint:
gateway:
enabled: true
# Access endpoints:
# GET /actuator/gateway/routes - List all routes
# GET /actuator/gateway/routes/{id} - Get specific route
# POST /actuator/gateway/refresh - Refresh routes
# GET /actuator/metrics/spring.cloud.gateway.requests - Request metrics
When to Use Spring Cloud Gateway
Use Spring Cloud Gateway when you need:
- A lightweight gateway for Spring-based microservices
- Reactive, high-performance routing
- Deep integration with Spring Security and Spring Cloud
- Programmatic route configuration
- Service discovery with Eureka or Consul
Consider alternatives like Kong, Istio, or AWS API Gateway for advanced features like API monetization, complex traffic management, or service mesh integration.
Final Thoughts
Spring Cloud Gateway is an excellent choice for microservice routing in Spring-based systems. It provides a simple, flexible, and reactive way to route traffic, apply filters, and secure your APIs. Start with basic routes, then enhance them with authentication, rate limiting, circuit breakers, and custom filters as your needs grow.
The gateway is often the first line of defense and the single entry point for your microservices architecture. Invest time in proper configuration, monitoring, and security to build a robust API layer.
To learn about service discovery integration, read Service Discovery with Spring Cloud Eureka. For reactive programming patterns, see Building Reactive APIs with Spring WebFlux. For detailed configuration guides, visit the Spring Cloud Gateway documentation.
1 Comment