Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 12

Designing the RateLimiter Interface

Recommended resources

Hello! Welcome to the next lesson in our journey to build a production-ready rate limiter.

In our previous lesson, we conducted a detailed comparison of the Fixed Window, Sliding Window, and Token Bucket algorithms. We concluded that each has distinct trade-offs in accuracy, memory usage, and burst handling, making them suitable for different scenarios. This highlights a key challenge in system design: building a system that is flexible enough to adapt to varying requirements.

Today, we will address that challenge head-on by designing the core architectural abstraction for our rate limiter. Our goal is to create a clean, extensible structure that allows us to use any of our algorithms interchangeably, without rewriting the core logic of our application. We'll achieve this by applying a powerful and widely-used software design pattern.

Your learning outcome for this lesson:

By the end of this lesson, you will be able to design a RateLimiter strategy interface to abstract algorithm implementations.



1. The Problem: Managing Multiple Algorithms

Imagine you've built your rate limiter interceptor. Inside it, you need to decide whether to allow a request. If your system needs to support multiple algorithms, you might be tempted to write code like this:

// Inside your request interceptor...
String algorithmType = "TOKEN_BUCKET"; // This would come from configuration

if ("FIXED_WINDOW".equals(algorithmType)) {
    // ... logic for fixed window ...
} else if ("SLIDING_WINDOW".equals(algorithmType)) {
    // ... logic for sliding window ...
} else if ("TOKEN_BUCKET".equals(algorithmType)) {
    // ... logic for token bucket ...
} else {
    // ... default or error ...
}

While this works for two or three algorithms, it quickly becomes unwieldy. Adding a new algorithm requires modifying this central block of code, which violates the Open-Closed Principle (software entities should be open for extension, but closed for modification). This design is brittle and hard to maintain.

The solution is to use the Strategy Design Pattern.


2. The Strategy Design Pattern

The Strategy pattern is a behavioral design pattern that enables selecting an algorithm at runtime. It involves defining a family of algorithms, encapsulating each one into a separate class, and making them interchangeable.

To understand this pattern in-depth, please watch the following video. It uses a payment processing example, which is an excellent and intuitive analogy for what we are about to build.

The Strategy Pattern Explained and Implemented in Java | Behavioral Design Patterns | Geekific

The video 'The Strategy Pattern Explained and Implemented in Java' from the Geekific channel provides a clear and concise explanation of the pattern's purpose, structure, and benefits.

Please watch from 00:48 to 05:11. Focus on these key concepts: The problem created by complex conditional logic (if-else statements). The definition of the Strategy pattern. The three main components: the Context, the Strategy Interface, and the Concrete Strategies. How these components interact, as shown in the class diagram.

As the video explains, the pattern consists of three key parts:

  1. The Strategy Interface: A common interface for all supported algorithms. It defines the method(s) that the Context will call.
  2. Concrete Strategies: Individual classes that implement the Strategy Interface, each providing the logic for a specific algorithm.
  3. The Context: A class that is configured with a Concrete Strategy object. It maintains a reference to a Strategy object and delegates the work to it, without knowing the details of how the strategy is implemented.


3. Applying the Strategy Pattern to Our Rate Limiter

Now, let's map these concepts directly onto our rate limiter project.

Strategy Method Design Pattern in Java
This flowchart shows the general structure of the Strategy pattern. For our project, the **Strategy Interface** will be our `RateLimiter` interface, the **Concrete Classes** will be `FixedWindowRateLimiter`, `TokenBucketRateLimiter`, etc., and the **Context Class** will be the Spring `HandlerInterceptor` that uses the selected rate limiter.

Designing the RateLimiter Interface

The cornerstone of our design is the RateLimiter strategy interface. It must define a single, clear contract: given the details of a request, can it proceed?

A good design passes not just the client identifier (key) but also the specific rules for that key. This makes our algorithm implementations pure and stateless, as they don't need to know where their configuration comes from.

Let's examine a well-structured example of such an interface.

Pluggable Rate Limiter Microservice with Spring Cloud Gateway

The article 'Pluggable Rate Limiter Microservice with Spring Cloud Gateway' by Dinesha Arney proposes a very similar architecture to what we are building. It provides an excellent starting point for our interface design.

Please read the first part of the section titled '3.3 Pluggable Rate Limiting Algorithms'. Start from 'To support multiple algorithms, we define a strategy interface...' and read down to just before the 'Pluggability' subsection. Pay close attention to the RateLimiterAlgorithm interface definition and the conceptual descriptions of how Token Bucket and Fixed Window would implement it.

Based on this, we can define our own RateLimiter interface. We will also need a simple Plain Old Java Object (POJO) to represent the rules.

// A simple data class to hold our configuration rules.
// We will expand on this in a future lesson on YAML configuration.
public class RateLimitRule {
    private int limit;
    private int windowSizeInSeconds;
    // ... other algorithm-specific fields like token bucket capacity, refill rate etc.
    
    // Getters and setters
}

// Our Strategy Interface
public interface RateLimiter {
    /**
     * Determines if a request should be allowed based on a given key and rule.
     *
     * @param key The unique identifier for the entity being rate-limited (e.g., IP address, API key).
     * @param rule The configuration rule to apply for this key.
     * @return true if the request is allowed, false otherwise.
     */
    boolean allowRequest(String key, RateLimitRule rule);
}

With this interface, we can now create our Concrete Strategies:

  • FixedWindowRateLimiter implements RateLimiter
  • SlidingWindowRateLimiter implements RateLimiter
  • TokenBucketRateLimiter implements RateLimiter

Each class will contain the specific logic for its algorithm, fulfilling the contract defined by RateLimiter.



4. A Practical Design Walkthrough

To see how this all comes together in a complete design, let's watch a video that walks through the low-level design of a rate limiter service. The presenter uses an abstract class instead of an interface, but the principle of abstracting the algorithm is identical. An interface is often preferred in Java as it allows a class to implement multiple strategies if needed and avoids forcing a class hierarchy.

Design Rate Limiter (LLD) - Token Bucket, Fixed & Sliding Window with Thread Safety

The video 'Design Rate Limiter (LLD)' by Shubh Patel provides a practical class diagram and code structure that directly applies the strategy pattern to our exact problem.

Watch the segment from 05:20 to 11:36. Focus on the class diagram and how the abstract RateLimiter class defines a common allowRequest method. This forces all concrete implementations (TokenBucket, FixedWindow, etc.) to provide their own version of that method, neatly separating the concerns of each algorithm.


5. The Context: Selecting the Right Strategy

The final piece is the Context. In our Spring Boot application, this will be a service or, more likely, our HandlerInterceptor. This component is responsible for:

  1. Identifying the incoming request (e.g., by IP address or API key).
  2. Looking up the appropriate RateLimitRule for that request from our configuration.
  3. Finding the correct RateLimiter implementation (the "strategy") based on the rule.
  4. Calling the allowRequest method on the selected strategy.

Let's look at one final example that shows this "resolver" or "context" logic in action.

Mastering Rate Limiting in Spring Boot: A Complete Guide to ...

The article 'Mastering Rate Limiting in Spring Boot' by Abdelghani El Amel demonstrates a very clean way to manage and select the appropriate strategy using Spring's dependency injection.

Please read 'Step 3: Implement Strategy-Specific Handlers' and 'Step 4: Determining the right Implementation to use per request'. Notice how each RateLimiterHandler implementation is a Spring @Component. The RateLimiterResolver then gets a Map of all these implementations injected by Spring, allowing it to easily pick the right one at runtime based on the client's configuration.

This approach is powerful because you can add a new rate-limiting algorithm just by creating a new class that implements RateLimiter and annotating it as a Spring @Component. The resolver will automatically discover and use it without any changes to its own code.



Key Takeaways

  • The Strategy Design Pattern is the ideal solution for managing multiple, interchangeable algorithms in a clean and extensible way.
  • The pattern consists of a Strategy Interface (RateLimiter), Concrete Strategies (FixedWindowRateLimiter, etc.), and a Context (our interceptor/resolver).
  • Our RateLimiter interface will have a method like allowRequest(String key, RateLimitRule rule) to keep algorithm implementations decoupled from configuration management.
  • This design adheres to key software principles like Single Responsibility (each class does one thing) and Open-Closed (we can add new algorithms without modifying existing code).

Next Steps

We have now designed the first major abstraction for our system: the RateLimiter interface, which answers the question, "Is this request allowed based on a chosen algorithm?"

In the next lesson, we will design the second key abstraction: the RateLimitKeyResolver interface. This will answer the complementary question, "What is the unique identifier for this specific request?" Together, these two interfaces will form the flexible, multi-tenant foundation of our rate limiter engine.