Hello! Welcome back to your course on building a rate limiter engine.
In our last lesson, we fortified our application by integrating Jakarta Bean Validation, ensuring that our rate limiter only starts up with a valid and complete configuration. We now have a robust RateLimiterProperties object, loaded at startup, that safely represents all our defined rules.
Today, we will build the brain of our rate limiter. A static configuration is useful, but we need a dynamic component that can look at an incoming request and decide which rule—if any—applies to it.
Your learning outcome for this lesson:
You will create a service that selects the appropriate rate limit rule based on the resolved request key. This service will be the central orchestrator, connecting our static YAML configuration to the dynamic, per-request decision-making process. It's the first major step in bringing our multi-tenant limiting strategy to life.
1. The Role of the Rule Lookup Service
At the heart of any configurable system lies a component responsible for interpreting those configurations at runtime. For our rate limiter, this component is a service whose sole responsibility is to find the best matching rule for any given request.
This service will:
- Receive details about the incoming
HttpServletRequest(e.g., its path, headers, method). - Iterate through the list of
Ruleobjects loaded from ourRateLimiterProperties. - Apply matching logic to determine which rules are applicable.
- Use a deterministic strategy to select the single best rule if multiple rules match.
- Return this rule (or an empty result if no rules match) to the caller.
Architecturally, this service acts as a manager or a lookup component, decoupling the configuration from the execution logic.
Pluggable Rate Limiter Microservice with Spring Cloud ...
The article 'Pluggable Rate Limiter Microservice' provides a great high-level overview of such an architecture. It describes the role of a service that manages rules and selects the appropriate one for a request.
Please read the sections 'Request Handling Flow' and 'Rate Limiter Service / Manager'. Focus on these key ideas: Step 2 of the flow: 'It looks up the configured rate limiting rule for the given route and client.' The manager's responsibility: 'On each request, determine which rule applies (matching the route and possibly client classification).'
This design keeps our interceptor (which we'll update later) clean. The interceptor's job will simply be to ask this new service, "Which rule should I apply for this request?" and then act on the answer.
2. A Tale of Two Patterns: Strategy and Factory
Before we jump into the code, let's consider the design patterns that guide our solution. Your project goal emphasizes building a system that can handle multiple rate-limiting algorithms, which is a perfect use case for the Strategy and Factory patterns.
-
Strategy Pattern: In Module 2, we designed a
RateLimiterinterface. This is the Strategy pattern in action. Each algorithm (Token Bucket, Sliding Window) will be a concrete implementation of this interface, making them interchangeable. -
Factory Pattern: How do we choose which strategy to use? This is where the Factory pattern comes in. A factory is an object whose job is to create other objects. In our case, a
RateLimiterFactorywill be responsible for creating the correctRateLimiterinstance based on theRulethat we select for a given request.
The service we are building today—the rule selector—is the essential prerequisite for the factory. It finds the "specification" (the Rule), which the factory will then use to build the "product" (the RateLimiter instance).
This short video provides an excellent explanation of how these two patterns work together.
Strategy and Factory Pattern, The Best Software Design Pattern Combo
The video 'Strategy and Factory Pattern, The Best Software Design Pattern Combo' shows how a factory can be used to dynamically create the correct strategy instance at runtime.
Please watch from the beginning to 02:06. The video uses a notification service as an example. Notice the analogy: The IOrderNotifier is the Strategy interface. EmailService, SMSService are the concrete Strategies. The OrderNotifyFactory is the Factory. It takes runtime information (a user object) and creates the correct notifier. Our system will be very similar: we'll have a factory that takes a Rule and creates the correct RateLimiter strategy.
Here is a visual representation of this combined pattern, which we will be building towards.

3. Implementing the RateLimiterRuleService
Now, let's implement the service that selects the best rule. We'll call it RateLimiterRuleService.
Step 1: Create the Service Class
First, create a new @Service class that gets the configured rules injected via its constructor.
import com.example.ratelimiter.config.RateLimiterProperties;
import com.example.ratelimiter.config.Rule;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class RateLimiterRuleService {
private final List<Rule> rules;
public RateLimiterRuleService(RateLimiterProperties properties) {
// We only instantiate the service if rate limiting is enabled.
// Spring's @ConditionalOnProperty could also be used on the class for this.
if (properties.enabled()) {
this.rules = properties.rules();
} else {
this.rules = List.of();
}
}
// We will implement the lookup logic here.
public Optional<Rule> findMatchingRule(HttpServletRequest request) {
// ...
}
}
Step 2: Implement Matching Logic with AntPathMatcher
The most complex part of matching is typically the request path, as rules might use wildcards (e.g., /api/** or /users/*/profile). Spring provides a utility class perfect for this: AntPathMatcher.
Our strategy will be:
- Filter the list of rules to find all that match the incoming request's path.
- From that filtered list, choose the most specific rule. For paths, "most specific" usually means the longest pattern. For example, if a request is for
/api/v1/admin, a rule for/api/v1/adminis more specific than a rule for/api/**.
The following reading provides a concrete code example of this exact logic.
Building a Per-Endpoint Rate Limiter Using the Token Bucket ...
The article 'Building a Per-Endpoint Rate Limiter' contains an excellent implementation of a service that selects rules based on the request path.
Please study the RateLimiterService.java class in this article. Pay close attention to the findBestMatch method. It demonstrates: The use of AntPathMatcher. Filtering a collection of patterns with pathMatcher.match(). Using max(Comparator.comparingInt(String::length)) to find the longest, most specific pattern.
Step 3: Putting It All Together
Let's integrate this logic into our RateLimiterRuleService. We'll create a findMatchingRule method that takes the HttpServletRequest and returns an Optional<Rule>. For now, our matching logic will focus only on the request path, but you can easily see how it could be extended to check headers, HTTP methods, or user principals.
import com.example.ratelimiter.config.RateLimiterProperties;
import com.example.ratelimiter.config.Rule;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Service;
import org.springframework.util.AntPathMatcher;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
@Service
public class RateLimiterRuleService {
private final List<Rule> rules;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
public RateLimiterRuleService(RateLimiterProperties properties) {
if (properties.enabled() && properties.rules() != null) {
// Sort rules by pattern specificity (length) descending.
// This is an optimization so we can find the first match later.
this.rules = properties.rules().stream()
.sorted(Comparator.comparing((Rule rule) -> rule.match().path().length()).reversed())
.toList();
} else {
this.rules = List.of();
}
}
/**
* Finds the most specific matching rule for the given request.
* @param request The incoming HTTP request.
* @return An Optional containing the best matching Rule, or empty if no rules match.
*/
public Optional<Rule> findMatchingRule(HttpServletRequest request) {
String requestPath = request.getRequestURI();
// Since we sorted the rules by specificity at construction time,
// the first rule that matches is guaranteed to be the most specific one.
return rules.stream()
.filter(rule -> matches(rule, requestPath))
.findFirst();
}
private boolean matches(Rule rule, String requestPath) {
// For now, we only match by path.
// This can be expanded to match on headers, methods, user roles, etc.
String pattern = rule.match().path();
if (pattern == null || pattern.isBlank()) {
return false; // A rule without a path pattern cannot match.
}
return pathMatcher.match(pattern, requestPath);
}
}
Note on the implementation: I've included an optimization. By sorting the rules from most to least specific once in the constructor, the findMatchingRule method becomes more efficient. It can just use findFirst() on the filtered stream, as the first match it encounters will always be the best one.
4. Activity: Implement and Test Your Service
Now it's your turn to build this component.
-
Create the
RateLimiterRuleService.javaclass in your project. -
Implement the logic as shown above, injecting
RateLimiterPropertiesand usingAntPathMatcher. -
To test your logic, you don't need a full integration test yet. You can reason about its behavior with a hypothetical
application.yml:rate-limiter: enabled: true rules: - id: "api-rule" match: path: "/api/**" algorithm: "TOKEN_BUCKET" params: { capacity: 100, refill-rate: 10 } - id: "admin-api-rule" match: path: "/api/admin/**" algorithm: "TOKEN_BUCKET" params: { capacity: 20, refill-rate: 1 }If a request comes in for
/api/admin/users, which rule should your service select? According to our "most specific match" logic, it should correctly chooseadmin-api-rule. The pre-sorting in the constructor ensures this happens efficiently.
Conclusion
Congratulations! You have just built the intelligent core of the rate limiter. This service is a crucial piece of the puzzle, responsible for dynamically applying your static configuration to live traffic.
Key Takeaways:
- A dedicated rule lookup service centralizes the logic for matching requests to configuration rules.
- Spring's
AntPathMatcheris the ideal tool for implementing flexible, wildcard-enabled path matching. - For systems with overlapping rules, a deterministic selection strategy like "most specific match first" is essential for predictable behavior.
- Our rule selection service is the first part of a Factory/Strategy pattern, setting the stage for creating specific
RateLimiteralgorithm instances.
Next Steps
We can now reliably determine which Rule applies to any given request. But what do we do with that rule? In our next lesson, you will implement a factory that provides the correct RateLimiter instance (in-memory vs. Redis) based on the rule. This factory will take the Rule object from today's service and construct the corresponding rate-limiting algorithm, ready for action.