Spring Cloud Gateway Architecture — Complete Production Setup Guide
How to build a microservices API Gateway with Spring Cloud Gateway. Routing, filters, JWT auth, rate limiting, circuit breaking, and load balancing — all with production-ready code.
Version Reference
Versions used in this post:
| Library | Version | Notes |
|---|---|---|
| Java | 21 | LTS |
| Spring Boot | 3.3.x | |
| Spring Cloud | 2023.0.3 (Leyton) | Compatible with Boot 3.3.x |
| Spring Cloud Gateway | 4.1.x | Included in Cloud 2023.0.x |
| Resilience4j | 2.2.x | Included in Cloud 2023.0.x |
| Reactor Netty | 1.1.x | Default WebFlux server |
Spring Boot ↔ Spring Cloud Compatibility
| Spring Boot | Spring Cloud | Codename |
|---|---|---|
| 3.3.x | 2023.0.x | Leyton |
| 3.2.x | 2023.0.x | Leyton |
| 3.1.x | 2022.0.x | Kilburn |
| 3.0.x | 2022.0.x | Kilburn |
| 2.7.x | 2021.0.x | Jubilee |
Always verify compatibility at the Spring Cloud release page.
Mismatched Boot and Cloud versions cause bean conflicts and auto-configuration errors.
What Is Spring Cloud Gateway?
The successor to Netflix Zuul. A non-blocking async API Gateway built on Spring WebFlux (Reactor).
Legacy Zuul 1.x → Servlet-based, blocking I/O
Spring Cloud Gateway → WebFlux-based, non-blocking I/O (2–3x throughput)
Three Core Concepts
Route : Where to send the request (id, uri, predicate, filter)
Predicate : Request matching condition (Path, Header, Method, Query...)
Filter : Request/response processing (auth, logging, rate limit, transformation...)
Flow:
Client Request
↓
Gateway Handler Mapping (Predicate matching)
↓
Gateway Web Handler
↓
Pre Filters → Proxied Service → Post Filters
↓
Client Response
1. Project Setup
<!-- pom.xml -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
</parent>
<properties>
<java.version>21</java.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
</properties>
<dependencies>
<!-- Spring Cloud Gateway (WebFlux-based; remove spring-boot-starter-web) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Eureka Client — required for lb://SERVICE-NAME routing -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Resilience4j circuit breaker (Reactor-based) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
<!-- Redis rate limiter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- JWT parsing -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</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>
Note: Spring Cloud Gateway is incompatible with Spring MVC (spring-boot-starter-web). Remove it.
2. Routing Configuration
YAML (Recommended)
spring:
cloud:
gateway:
routes:
# User service
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/v1/users/**
filters:
- StripPrefix=2
- name: CircuitBreaker
args:
name: userServiceCB
fallbackUri: forward:/fallback/user
# Order service
- id: order-service
uri: lb://ORDER-SERVICE
predicates:
- Path=/api/v1/orders/**
- Method=GET,POST
filters:
- StripPrefix=2
- AddRequestHeader=X-Gateway-Source, spring-cloud-gateway
# Payment service (rate limited + required header)
- id: payment-service
uri: lb://PAYMENT-SERVICE
predicates:
- Path=/api/v1/pay/**
- Header=X-Request-ID, .+
filters:
- StripPrefix=2
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"
# Internal-only route (block external access)
- id: internal-block
uri: no://op
predicates:
- Path=/api/internal/**
filters:
- name: SetStatus
args:
status: 403
Java Config
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r
.path("/api/v1/users/**")
.filters(f -> f
.stripPrefix(2)
.addRequestHeader("X-Gateway-Source", "spring-cloud-gateway")
.circuitBreaker(c -> c
.setName("userServiceCB")
.setFallbackUri("forward:/fallback/user")
)
)
.uri("lb://USER-SERVICE")
)
.build();
}
}
3. JWT Auth Filter
Validate JWTs at the Gateway to eliminate duplicated auth logic in backend services.
@Component
@RequiredArgsConstructor
public class JwtAuthFilter implements GlobalFilter, Ordered {
private final JwtTokenProvider jwtTokenProvider;
private static final List<String> WHITELIST = List.of(
"/api/v1/auth/login",
"/api/v1/auth/signup",
"/api/v1/auth/refresh",
"/actuator/health"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
if (WHITELIST.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
String authHeader = exchange.getRequest()
.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return unauthorized(exchange, "Authorization header missing");
}
String token = authHeader.substring(7);
try {
Claims claims = jwtTokenProvider.validateAndGetClaims(token);
ServerHttpRequest mutatedRequest = exchange.getRequest()
.mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Role", claims.get("role", String.class))
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (ExpiredJwtException e) {
return unauthorized(exchange, "Token expired");
} catch (JwtException e) {
return unauthorized(exchange, "Invalid token");
}
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = """
{"error": "UNAUTHORIZED", "message": "%s"}
""".formatted(message);
DataBuffer buffer = response.bufferFactory()
.wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -100;
}
}
4. Rate Limiting (Redis-backed)
spring:
data:
redis:
host: redis
port: 6379
cloud:
gateway:
routes:
- id: api-route
uri: lb://API-SERVICE
predicates:
- Path=/api/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
@Configuration
public class RateLimiterConfig {
@Bean
public KeyResolver userKeyResolver() {
return exchange -> {
String userId = exchange.getRequest()
.getHeaders()
.getFirst("X-User-Id");
return Mono.just(userId != null ? userId : "anonymous");
};
}
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
Objects.requireNonNull(
exchange.getRequest().getRemoteAddress()
).getAddress().getHostAddress()
);
}
}
When rate limit is exceeded:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Remaining: 0
X-RateLimit-Replenish-Rate: 100
X-RateLimit-Burst-Capacity: 200
5. Circuit Breaker (Resilience4j)
resilience4j:
circuitbreaker:
instances:
userServiceCB:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 3
minimumNumberOfCalls: 5
orderServiceCB:
slidingWindowSize: 20
failureRateThreshold: 30
waitDurationInOpenState: 30s
timelimiter:
instances:
userServiceCB:
timeoutDuration: 3s
orderServiceCB:
timeoutDuration: 5s
@RestController
@RequestMapping("/fallback")
public class FallbackController {
@GetMapping("/user")
public ResponseEntity<Map<String, String>> userFallback() {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Map.of(
"error", "USER_SERVICE_UNAVAILABLE",
"message", "User service is temporarily unavailable. Please try again later."
));
}
}
6. Request/Response Logging Filter
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String requestId = UUID.randomUUID().toString().substring(0, 8);
long startTime = System.currentTimeMillis();
log.info("[{}] {} {} | IP: {} | User: {}",
requestId,
request.getMethod(),
request.getPath(),
getClientIp(request),
request.getHeaders().getFirst("X-User-Id")
);
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long duration = System.currentTimeMillis() - startTime;
log.info("[{}] {} {} → {} ({}ms)",
requestId,
request.getMethod(),
request.getPath(),
exchange.getResponse().getStatusCode(),
duration
);
}));
}
@Override
public int getOrder() {
return -200;
}
}
7. CORS Configuration
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns:
- "https://testforge.kr"
- "https://*.testforge.kr"
- "http://localhost:3000"
allowedMethods: [GET, POST, PUT, DELETE, OPTIONS]
allowedHeaders: [Authorization, Content-Type, X-Request-ID]
exposedHeaders: [X-RateLimit-Remaining]
allowCredentials: true
maxAge: 3600
8. Full Architecture Diagram
┌─────────────────────────────────────┐
│ Spring Cloud Gateway │
│ │
Client ──────────│──→ LoggingFilter (order: -200) │
│──→ JwtAuthFilter (order: -100) │
│──→ RateLimitFilter │
│──→ CircuitBreakerFilter │
│──→ Route Matching │
└──────────────┬──────────────────────┘
│ lb://SERVICE-NAME
┌────────┴────────┐
│ Eureka / K8s │ Service Discovery
└────────┬────────┘
┌─────────────────┼──────────────────┐
↓ ↓ ↓
[USER-SERVICE] [ORDER-SERVICE] [PAYMENT-SERVICE]
Kong vs Spring Cloud Gateway
| Situation | Recommendation |
|---|---|
| Spring Boot microservices team | Spring Cloud Gateway |
| Mixed language/framework environment | Kong / Nginx |
| Rich plugin ecosystem needed | Kong |
| Reactive streaming | Spring Cloud Gateway |
| Code-level customization | Spring Cloud Gateway |
| Managed by a separate infra team | Kong / AWS API Gateway |
Within the Spring ecosystem, Spring Cloud Gateway is the most natural fit.
JWT filters, Rate Limiters, and Circuit Breakers are all managed as code using Spring standards, resulting in high maintainability.