Service Discovery, Config & Gateway

Spring Cloud Gateway

18 min Lesson 7 of 12

Spring Cloud Gateway

In the previous lesson you learned the theoretical case for an API Gateway. This lesson gets practical: you will build a working gateway with Spring Cloud Gateway (the reactive, non-blocking implementation built on Spring WebFlux and Project Reactor). By the end you will understand how to declare routes, compose predicates, apply filters, and reason about the security and operational implications of every decision.

Adding the Dependency

Spring Cloud Gateway is a separate starter. Create a new Spring Boot 3 project (or module) and add it to pom.xml:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
Do NOT add spring-boot-starter-web alongside the gateway starter. Spring Cloud Gateway is built on WebFlux (reactive, non-blocking). Mixing it with the servlet-based spring-boot-starter-web on the same classpath causes a startup conflict. Use only the gateway starter; if you need security add spring-boot-starter-security and the reactive variant of Security will be configured automatically.

The Route: The Fundamental Building Block

A route is the core concept in Spring Cloud Gateway. Each route has three parts:

  1. ID — a unique string used in logs and metrics.
  2. URI — the destination to forward matched requests to.
  3. Predicates — conditions that must ALL be true for the route to match a request.
  4. Filters — transformations applied to the request before forwarding or to the response before returning it.

Routes can be declared in application.yml (most common), in a RouteLocator Java bean, or dynamically via a discovery client. Start with YAML:

spring: cloud: gateway: routes: - id: order-service-route uri: http://localhost:8081 predicates: - Path=/api/orders/** filters: - StripPrefix=1

What this does: any request whose path starts with /api/orders/ is forwarded to http://localhost:8081. The StripPrefix=1 filter removes the first path segment (/api) before forwarding, so GET /api/orders/42 becomes GET /orders/42 on the downstream service.

Predicates in Depth

A predicate is a boolean test against the incoming ServerWebExchange. Spring Cloud Gateway ships with a rich set of built-in predicates. Multiple predicates on one route are ANDed together.

Path predicate — matches on URL path with Ant-style wildcards:

predicates: - Path=/api/products/**, /api/catalog/**

Method predicate — restricts by HTTP verb:

predicates: - Method=GET,HEAD

Header predicate — matches when a header exists and its value satisfies a regular expression:

predicates: - Header=X-Request-Source, internal-.*

Query predicate — matches when a query parameter is present (and optionally matches a regex):

predicates: - Query=version, v[23]

After / Before / Between predicates — time-windowed routing, useful for scheduled maintenance or feature flags:

predicates: - Between=2024-01-01T00:00:00+00:00[UTC], 2025-01-01T00:00:00+00:00[UTC]

Weight predicate — used for canary deployments; routes a percentage of traffic to a new version:

spring: cloud: gateway: routes: - id: product-service-v1 uri: http://localhost:8082 predicates: - Path=/api/products/** - Weight=product-group, 90 - id: product-service-v2 uri: http://localhost:8083 predicates: - Path=/api/products/** - Weight=product-group, 10
Predicate order matters. Routes are evaluated in declaration order and the first match wins. Put more specific routes before more general ones, just like catch blocks in Java. A broad Path=/** catch-all route should always be last.

Gateway Filters in Depth

Filters operate on the exchange. GatewayFilter applies to a single route; GlobalFilter applies to every route. Built-in filters cover the most common gateway concerns.

AddRequestHeader — inject a header before forwarding:

filters: - AddRequestHeader=X-Gateway-Source, my-gateway

Downstream services can check this header to ensure requests came through the gateway, not directly.

AddResponseHeader — inject a header on the way back:

filters: - AddResponseHeader=X-Content-Type-Options, nosniff

RewritePath — full regex-based path rewriting, more powerful than StripPrefix:

filters: - RewritePath=/api/v1/(?<segment>.*), /${segment}

RequestRateLimiter — built-in token-bucket rate limiting backed by Redis. Add the Redis reactive starter, then configure:

filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 50 # tokens added per second redis-rate-limiter.burstCapacity: 100 # max tokens in the bucket redis-rate-limiter.requestedTokens: 1 # tokens consumed per request key-resolver: "#{@ipKeyResolver}" # Spring SpEL bean reference

The key resolver determines how to identify a caller. The simplest resolver uses the remote IP address:

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 { @Bean public KeyResolver ipKeyResolver() { return exchange -> Mono.just( exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() ); } }

In production, resolve on a JWT subject or API key instead of IP so that NAT addresses do not unfairly share a bucket.

CircuitBreaker filter — integrates with Resilience4j to open the circuit when a downstream service is failing:

filters: - name: CircuitBreaker args: name: order-cb fallbackUri: forward:/fallback/orders

Writing a Custom GatewayFilter

When built-in filters are insufficient you can write your own. Implement GatewayFilterFactory and register it as a Spring bean:

package com.example.gateway.filter; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @Component public class ApiKeyGatewayFilterFactory extends AbstractGatewayFilterFactory<ApiKeyGatewayFilterFactory.Config> { public ApiKeyGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { String apiKey = exchange.getRequest().getHeaders() .getFirst("X-Api-Key"); if (config.getRequiredKey().equals(apiKey)) { return chain.filter(exchange); } exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); }; } public static class Config { private String requiredKey; public String getRequiredKey() { return requiredKey; } public void setRequiredKey(String requiredKey) { this.requiredKey = requiredKey; } } }

Use the factory by name in YAML (the class name without GatewayFilterFactory suffix):

filters: - name: ApiKey args: requiredKey: ${INTERNAL_API_KEY}
The bean naming convention is load-bearing. Spring Cloud Gateway discovers GatewayFilterFactory beans by class name. The YAML key must exactly match the prefix before GatewayFilterFactory (case-sensitive). Getting this wrong results in a FilterDefinitionNotFoundException at startup.

Java DSL Routes

YAML is convenient for simple routes, but the Java DSL gives you full IDE support, type safety, and programmatic logic (e.g. reading routes from a database):

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; @Configuration public class GatewayConfig { @Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes() .route("order-service", r -> r .path("/api/orders/**") .filters(f -> f .stripPrefix(1) .addRequestHeader("X-Gateway-Source", "my-gateway") .circuitBreaker(c -> c .setName("order-cb") .setFallbackUri("forward:/fallback/orders") ) ) .uri("lb://order-service") // lb:// = load-balanced via Eureka ) .build(); } }

The lb://order-service URI scheme tells the gateway to resolve the service name through the Eureka registry and load-balance across its instances using Spring Cloud LoadBalancer — the same mechanism covered in Lesson 3.

Security Implications

  • Never trust headers injected by callers. If downstream services rely on X-User-Id or X-Roles headers forwarded by the gateway, strip those headers from the incoming request first, then add your own after authentication. Otherwise any client can forge them.
  • Gateway is not a firewall. Ensure downstream services are not directly reachable from outside your private network. The gateway is the single ingress point by policy and by network topology — not just by convention.
  • Log correlation IDs. Add a GlobalFilter that generates a UUID per request, attaches it as a header (X-Correlation-Id), and passes it downstream. This makes distributed tracing across multiple services possible.

Summary

Spring Cloud Gateway is configured around routes, each of which matches incoming requests via composable predicates and transforms them via filters. Predicates cover path, method, header, query, time, and weight-based traffic splitting. Built-in filters handle path rewriting, header injection, rate limiting, and circuit breaking; custom GatewayFilterFactory beans let you add any cross-cutting logic you need. Use the lb:// URI scheme to integrate seamlessly with Eureka-registered services. In the next lesson you will see how authentication, CORS, logging, and rate limiting are layered on top of the gateway as cross-cutting concerns.