Hello! Welcome to the fifth lesson in our module on Inter-Service Communication Patterns.
In our last lesson, we established the critical need for service discovery in a dynamic microservices environment. We contrasted the client-side discovery pattern (where the client is "smart") with the server-side pattern (where a central router is "smart"). You learned that server-side discovery simplifies clients and centralizes control, often implemented by a component known as an API Gateway.
Today, we transition from theory to practice. We will build that central router. By the end of this lesson, you will be able to implement an API Gateway using Spring Cloud Gateway for routing, rate limiting, and request transformation. This is a cornerstone skill for building production-ready microservices and a frequent topic in system design interviews.
1. The Role of an API Gateway
Before we dive into code, let's solidify our understanding of what an API Gateway does and why it's indispensable. In a microservices architecture, the gateway acts as the single entry point for all external client requests. It's a façade that hides the complexity of the internal system.
To get a comprehensive overview of the gateway's responsibilities, let's watch a segment from a talk at Spring I/O.
Making Spring Cloud Gateway your perfect API gateway solution by Dan Erez @ Spring I/O 2024
The talk 'Making Spring Cloud Gateway your perfect API gateway solution' by Dan Erez provides an excellent introduction to the necessity and core functions of an API Gateway.
Watch the section from 02:27 to 08:53. As you watch, pay attention to the list of common gateway functionalities like routing, authentication, caching, and flow control. These are the cross-cutting concerns that a gateway centralizes.
As the video highlighted, an API Gateway is much more than a simple reverse proxy. It handles a suite of critical tasks, allowing your downstream microservices to focus purely on their business logic.
2. Introducing Spring Cloud Gateway
In the Spring ecosystem, the modern, go-to solution for building an API Gateway is Spring Cloud Gateway. It's built on a reactive foundation (Project Reactor, Spring WebFlux, and Netty), making it non-blocking and capable of handling high concurrency with minimal resource usage—a perfect fit for an edge service that must be highly performant and resilient.
The architecture of Spring Cloud Gateway is based on a few simple concepts:
- Route: The basic building block. It has a unique ID, a destination URI, a collection of predicates, and a collection of filters.
- Predicate: A condition that must be met for a request to be matched to a route. This could be based on the request path, host, headers, etc.
- Filter: Logic that can modify the incoming request before it's routed or the outgoing response after it returns from the downstream service.

You can configure routes in two ways:
- Declaratively: Using your
application.ymlorapplication.propertiesfile. This is the most common and straightforward method. - Programmatically: By defining a
RouteLocatorSpring bean. This offers more power for dynamic or conditional routing logic.
We will focus on the declarative approach as it's sufficient for most use cases and is easier to manage.
3. Implementing Core Functionality
Let's implement the three key features mentioned in our learning outcome: routing, transformation, and rate limiting.
Step 1: Project Setup
To create a Spring Cloud Gateway service, you start a new Spring Boot project and add the spring-cloud-starter-gateway dependency. Since our gateway will eventually discover other services, we'll also include the Eureka client dependency we discussed in the last lesson.
In your pom.xml, you would include:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
And in your main application class, enable discovery:
@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
Step 2: Implementing Routing
Routing is the gateway's primary job. Let's say we have a product-service and an order-service. We want to route requests starting with /products/** to the product service and /orders/** to the order service.
The uri property is key here. While you can use a static URL like http://localhost:8081, the real power comes from integrating with service discovery. By using lb://SERVICE-NAME, we tell the gateway to look up the service in Eureka (or another service registry) and load-balance the request across available instances.
Here is the configuration in application.yml:
spring:
application:
name: api-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # Enables discovery integration
lower-case-service-id: true
routes:
- id: product-service-route
uri: lb://product-service # "lb" stands for load-balanced
predicates:
- Path=/products/**
- id: order-service-route
uri: lb://order-service
predicates:
- Path=/orders/**
With this configuration, a request to http://api-gateway:8080/products/123 will be forwarded to a healthy instance of product-service at the path /products/123.
Step 3: Implementing Request Transformation with Filters
Filters allow us to modify the request and response. This is essential for adapting requests from external clients to what internal services expect.
Let's enhance our order-service route. Imagine the service's endpoints are actually prefixed with /api/v1, so it expects requests like /api/v1/orders/456. The external API, however, should be clean (/orders/456). We can use filters to handle this transformation.
StripPrefix=1: This filter removes the first segment of the path. So,/orders/456becomes/456.PrefixPath=/api/v1/orders: This filter adds a prefix to the path.
Combining these, we can transform /orders/456 into /api/v1/orders/456.
Another common use case is adding headers, for example, to trace a request's origin.
spring:
cloud:
gateway:
routes:
- id: order-service-route
uri: lb://order-service
predicates:
- Path=/orders/**
filters:
# Transformation 1: Rewrite the path
- StripPrefix=1
- PrefixPath=/api/v1/orders
# Transformation 2: Add a request header
- AddRequestHeader=X-Request-Source, api-gateway
This configuration now does the following for a request to /orders/456:
- Matches the
Path=/orders/**predicate. - Applies the
StripPrefix=1filter, changing the path to/456. - Applies the
PrefixPathfilter, changing the path to/api/v1/orders/456. - Applies the
AddRequestHeaderfilter, addingX-Request-Source: api-gateway. - Forwards the modified request to the
order-service.
You can even modify the request or response body. This is a more advanced feature, but it's incredibly powerful for tasks like masking sensitive data in a response before it leaves your system.
Making Spring Cloud Gateway your perfect API gateway solution by Dan Erez @ Spring I/O 2024
Let's return to the Spring I/O talk to see a live demonstration of modifying the request and response body.
Watch the segment from 23:52 to 26:17. Observe how the presenter uses filters to uppercase a request body and mask sensitive credit card information in a response body. This showcases the power of gateway filters beyond simple path and header manipulation.
Step 4: Implementing Rate Limiting
A critical responsibility of a gateway is to protect your backend services from being overwhelmed. Rate limiting restricts how many requests a client can make in a given time period.
Spring Cloud Gateway provides a RequestRateLimiter filter that works with Redis to implement a distributed rate limiter based on the Token Bucket algorithm.

To see how this is implemented step-by-step, let's watch a detailed tutorial.
Rate Limiter using Spring Cloud Gateway and Redis example | Tech Primers
The video 'Rate Limiter using Spring Cloud Gateway and Redis example' by Tech Primers is a complete walkthrough of this feature. It covers everything from dependencies to testing.
Watch this video from start to finish (00:00 - 14:54). It will guide you through: Adding the spring-boot-starter-data-redis-reactive dependency. Configuring the RequestRateLimiter filter in YAML with replenishRate (requests per second) and burstCapacity (max burstable requests). Crucially, implementing a KeyResolver bean. This bean defines how to identify a client for rate limiting (e.g., by IP address, API key, or user ID from a JWT token). Testing the implementation and seeing the 429 Too Many Requests responses. Understanding the architectural benefit of handling this at the gateway.
Let's summarize the key implementation pieces from the video.
-
Add Redis Dependency:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> -
Configure the Filter in
application.yml:spring: cloud: gateway: routes: - id: rate-limited-route uri: lb://some-service predicates: - Path=/api/limited/** filters: - name: RequestRateLimiter args: # Tells the filter to use our custom KeyResolver bean key-resolver: "#{@ipKeyResolver}" # The rate limiter implementation (provided by Spring) redis-rate-limiter.replenishRate: 10 # 10 requests per second redis-rate-limiter.burstCapacity: 20 # Allows a burst of 20 requests -
Implement the
KeyResolver: This is the most important part from a design perspective. It determines the "key" for rate limiting. Here's a common implementation that uses the client's IP address.import org.springframework.cloud.gateway.filter.ratelimiter.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Configuration public class GatewayConfig { @Bean public KeyResolver ipKeyResolver() { // Return a Mono<String> representing the key. // In an interview, you could discuss using a JWT claim instead. return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); } }The name of this bean,
ipKeyResolver, is referenced in thekey-resolverargument in our YAML file.
Test your understanding!
You are tasked with configuring a new route for a payment-service. The requirements are:
- Incoming requests with the path
/payments/**should be routed to thepayment-service. - The downstream
payment-serviceexpects the path to be/v2/process-payment/**. For example, a request to/payments/charge/123should be forwarded as/v2/process-payment/charge/123. - A rate limit of 2 requests per second, with a burst of 5, should be applied to each unique authenticated user. Assume the user's ID is available in a JWT claim named
sub.
How would you configure the application.yml and the KeyResolver bean to meet these requirements?
Show answer
application.yml configuration:
spring:
cloud:
gateway:
routes:
- id: payment-service-route
uri: lb://payment-service
predicates:
- Path=/payments/**
filters:
# Rewrite /payments/** to /v2/process-payment/**
- RewritePath=/payments/(?<segment>.*), /v2/process-payment/$\{segment}
# Apply rate limiting using a bean named 'userKeyResolver'
- name: RequestRateLimiter
args:
key-resolver: "#{@userKeyResolver}"
redis-rate-limiter.replenishRate: 2
redis-rate-limiter.burstCapacity: 5
Note: The RewritePath filter uses a regular expression to capture the path segment after /payments/ and re-uses it in the replacement string.
KeyResolver bean implementation:
This implementation would require parsing the JWT. While the full JWT parsing logic is omitted for brevity, the key is to extract the sub claim.
import org.springframework.cloud.gateway.filter.ratelimiter.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
public class GatewayConfig {
@Bean
public KeyResolver userKeyResolver() {
return exchange -> {
// This is a simplified example. In a real app, you would
// parse the JWT from the 'Authorization' header to get the 'sub' claim.
// For example: return getClaim(exchange, "sub");
// Here, we'll simulate getting it from a header for simplicity.
String userId = exchange.getRequest().getHeaders().getFirst("X-User-ID");
return Mono.just(userId != null ? userId : "anonymous");
};
}
}
This demonstrates the flexibility of the KeyResolver to enforce business-specific rate-limiting rules, a concept that is excellent to discuss in an interview.
Conclusion
Congratulations! You have now learned how to implement a fully functional API Gateway using Spring Cloud Gateway. This is a massive step towards building a robust and production-ready microservices system.
Key Takeaways:
- API Gateway as a Façade: It's the single entry point that centralizes cross-cutting concerns like routing, security, and rate limiting.
- Spring Cloud Gateway: A modern, reactive, and highly performant gateway solution built for the cloud.
- Routes, Predicates, and Filters: These are the core building blocks for configuring gateway behavior. You define what to match (predicates) and how to modify it (filters).
- Key Implementations:
- Routing: Use
Pathpredicates and a load-balanced URI (lb://service-name) to direct traffic. - Transformation: Use filters like
StripPrefix,RewritePath, andAddRequestHeaderto adapt requests. - Rate Limiting: Use the
RequestRateLimiterfilter with Redis and a customKeyResolverto protect your services.
- Routing: Use
Next Up
Our gateway can now route, transform, and protect our services. But what happens if the order-service goes down? The gateway will try to route requests to it, and they will fail. If enough requests are failing and tying up resources, this could bring down the gateway itself! This is known as a cascading failure.
In our next lesson, we will tackle this problem by learning to implement the Circuit Breaker pattern with a fallback mechanism using Resilience4j to prevent cascading failures.
Can't find a good explanation? Sign up and we'll make it for you
Sign up