Building a Fair API Playground with Spring Boot and Redis

Bouncers, Bandwidth, and Buckets: Rate Limiting APIs with Spring Boot and Redis

Building a Fair API Playground with Spring Boot and Redis

Rate limiting is like the bouncer at a popular nightclub. It helps keep things in check by managing the throng. Imagine if everyone could just rush in at once; the place would be a mess. Similarly, for APIs, controlling the flow of incoming requests is crucial. Otherwise, servers get overwhelmed, resources get consumed unfairly, and genuine users get the short end of the stick.

Now, the combination of Spring Boot and Redis offers a stellar setup for implementing rate limiting. They bring together powerful tools and libraries to create a robust system. Let’s dive into the world of APIs, rate limits, and distributed systems, all while keeping it simple and laid-back.

Getting Familiar with Rate Limiting

At its core, rate limiting is a method to control the number of requests a client can send to your API within a specific timeframe. On a busy day, many users might hit their favorite site’s API repeatedly. Without a gatekeeper, the site’s server could get overloaded. Rate limiting has a solution. It ensures fair play, balancing service accessibility for all users.

Different algorithms can manage rate limits, but the token bucket algorithm frequently gets the nod. Think of it like a watering can with a fixed capacity. Users can draw from it until it’s empty, at which point they have to wait until it refills.

Setting Up Your Spring Boot Project

So, you’re ready to set the stage? Start with a Spring Boot project. You can whip one up using Spring Initializr or any IDE that tickles your fancy. Add the necessary dependencies. The key players for rate limiting with Redis are the bucket4j library and the Redis client.

Here’s a glimpse at the dependencies you need:

<dependency>
    <groupId>com.github.vladimir-bukhtoyarov</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>3.1.0</version>
</dependency>
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
</dependency>

Getting Redis Ready

Before you dip into the code, make sure Redis is up and running. You might use Docker for a quick setup:

sudo docker run -d -p 6379:6379 redis

Building the Rate Limiting Service

The cornerstone of your rate limiting framework will be the RateLimitingService. This service manages the rate limit buckets for each client. It’s like a personal bartender for every client, ensuring no one overindulges.

Here’s a peek at what this service looks like:

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class RateLimitingService {

    private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

    public boolean allowRequest(String apiKey) {
        Bucket bucket = buckets.computeIfAbsent(apiKey, this::createNewBucket);
        return bucket.tryConsume(1);
    }

    private Bucket createNewBucket(String apiKey) {
        Bandwidth limit = Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(1)));
        return Bucket4j.builder().addLimit(limit).build();
    }
}

Every client (identified via API key) gets a bucket. The service tracks these buckets and determines if they can handle another request.

Sliding the Service into Spring Boot

Integrate this service with your Spring Boot app by creating a filter to check each incoming request. The filter acts like the nightclub’s bouncer, verifying if a client can enter based on the rate limit.

Here’s how that filter looks:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
@Order(1)
public class RateLimitFilter implements Filter {

    private final RateLimitingService rateLimitingService;

    @Autowired
    public RateLimitFilter(RateLimitingService rateLimitingService) {
        this.rateLimitingService = rateLimitingService;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String apiKey = httpRequest.getHeader("X-API-KEY");

        if (!rateLimitingService.allowRequest(apiKey)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(429);
            httpResponse.setContentType("application/json");
            httpResponse.getWriter().write("{\"Status\": \"429 TOO_MANY_REQUESTS\", \"Description\": \"API request limit linked to your current plan has been exhausted.\"}");
            return;
        }

        chain.doFilter(request, response);
    }
}

Requests get a once-over by this filter. If they’re not kosher (i.e., they exceed the rate limit), users get a 429 status code - a gentle reminder to cool down their request frenzy.

Making Rate Limits Distributed with Redis

If your server setup spans multiple instances, keeping track of buckets locally won’t cut it. You need a central brain, a role Redis can play brilliantly. This setup ensures every instance adheres to the same rate limit.

Update the RateLimitingService to use Redis for bucket management:

import io.lettuce.core.api.StatefulRedisConnection;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.distributed.proxy.DistributedProxyManager;
import io.github.bucket4j.distributed.proxy.GenericDistributedProxyManager;
import io.github.bucket4j.distributed.redis.RedisBackendBuilder;
import io.github.bucket4j.distributed.redis.RedisProxyManager;

public class RateLimitingService {

    private final DistributedProxyManager<String> proxyManager;

    public RateLimitingService(StatefulRedisConnection<String, String> redisConnection) {
        RedisBackendBuilder<String> builder = RedisBackendBuilder
                .of(redisConnection.sync())
                .build();
        proxyManager = new RedisProxyManager<>(builder);
    }

    public boolean allowRequest(String apiKey) {
        Bucket bucket = proxyManager.getProxy(apiKey, () -> createNewBucket());
        return bucket.tryConsume(1);
    }

    private Bucket createNewBucket() {
        Bandwidth limit = Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(1)));
        return Bucket4j.builder().addLimit(limit).build();
    }
}

This tweak ensures all rate limit data gets stored in Redis, keeping every server instance on the same page.

Testing Time

Setting up is only half the job. Ensuring it works—that’s where the rubber meets the road. Integration tests come in handy. Using Testcontainers, you can fire up a Redis instance and test the rate limiting setup. Here’s an example of how you could write tests:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Testcontainers
@SpringBootTest
@AutoConfigureMockMvc
public class RateLimitingTest {

    @Container
    private static final GenericContainer<?> redisContainer = new GenericContainer<>("redis:latest")
            .withExposedPorts(6379);

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testRateLimiting() throws Exception {
        for (int i = 0; i < 10; i++) {
            mockMvc.perform(get("/api/rate-limiting/resource")
                    .header("X-API-KEY", "test-api-key"))
                    .andExpect(status().isOk());
        }

        mockMvc.perform(get("/api/rate-limiting/resource")
                .header("X-API-KEY", "test-api-key"))
                .andExpect(status().isTooManyRequests());
    }
}

This test simulates making multiple requests to your API and ensures the rate limit kicks in as designed.

Wrapping It Up

Implementing rate limiting for your APIs using Spring Boot and Redis isn’t just a best practice—it’s essential for managing server load and ensuring all users get a fair shot at your services. With the bucket4j library and Redis, you can create a distributed, reliable rate limiting system that stands tall even under heavy traffic.

This setup will keep your APIs smooth sailing, fair, and secure—all the while standing ready to tackle anything thrown its way. Now go on, refine your APIs, and give your server the protection it deserves!