Configurable Rate Limiter in Spring Boot
Building a Configurable Rate Limiter in Spring Boot Using Kotlin

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.
Search for a command to run...
Building a Configurable Rate Limiter in Spring Boot Using Kotlin

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.
No comments yet. Be the first to comment.
A practical guide to choosing the right coroutine primitive for your Android/Kotlin projects

This guide explains what Channels are, when to use them, how to use them correctly, and when NOT to use them — in simple, professional language. 1. What problem do Channels solve? In Kotlin coroutines, you often have multiple coroutines running at t...

Learn how to build preview-safe ViewModels in Jetpack Compose using Hilt.

HandlerInterceptor vs Filter in Spring Boot: A Practical Guide with JNDI Injection Prevention

Simple In-Memory Caching: A Tiny Trick with Massive Impact

Let’s say you have an API that shortens URLs. Everything is going great until one day, a bot starts hitting your API thousands of times per minute. Your server struggles, real users get slow responses, and your database is overloaded. This is where rate limiting comes in.
Rate limiting helps:
✔️ Prevent abuse (e.g., spammers, bots)
✔️ Ensure fair usage (so one user doesn’t hog resources)
✔️ Protect your backend from overload
We’ll create a configurable rate limiter where:
✅ You define limits in application.properties (no hardcoded values)
✅ You can apply limits using @RateLimited annotation
✅ It works per IP and per API endpoint
✅ It resets automatically after a set time
application.propertiesInstead of hardcoding limits in the code, we’ll define them in application.properties:
rate-limiter.enabled=true
# Limits for different API endpoints
rate-limiter.limits.SHORTEN_URL.limit=50
rate-limiter.limits.SHORTEN_URL.timeFrameMinutes=1
rate-limiter.limits.ADVANCED_SHORTEN_URL.limit=30
rate-limiter.limits.ADVANCED_SHORTEN_URL.timeFrameMinutes=2
rate-limiter.limits.LINK_FOLIO.limit=20
rate-limiter.limits.LINK_FOLIO.timeFrameMinutes=5
What’s Happening Here?
The shorten URL API (/shorten) allows 50 requests per minute.
The advanced shorten URL API allows 30 requests per 2 minutes.
The Link Folio API allows 20 requests per 5 minutes.
We can change these values anytime without redeploying the app.
Spring Boot provides a way to read these values using @ConfigurationProperties:
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
@Configuration
@ConfigurationProperties(prefix = "rate-limiter")
class RateLimiterConfig {
var enabled: Boolean = true
var limits: Map<String, RateLimitProperties> = emptyMap()
data class RateLimitProperties(var limit: Int = 0, var timeFrameMinutes: Long = 0)
}
What’s Happening Here?
enabled: Turns rate limiting on/off dynamically.
limits: A map of API names to their rate limits.
Now, we can inject this RateLimiterConfig anywhere in the app.
@RateLimited AnnotationTo make things easy, let’s create an annotation that we can apply to API methods:
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class RateLimited(val service: String)
This allows us to do things like:
@RateLimited(service = "SHORTEN_URL")
@PostMapping("/shorten")
fun shortenUrl(@RequestBody request: UrlShortenRequest): ResponseEntity<String> {
return ResponseEntity.ok("Shortened URL")
}
Now, our rate limiter will know this API should be limited based on the SHORTEN_URL settings.
We need a service that will track API requests per IP address and per service type.
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.util.*
import java.util.concurrent.*
@Service
class RateLimiterService(private val config: RateLimiterConfig) {
private val requestMap = ConcurrentHashMap<String, ConcurrentHashMap<String, Pair<Int, LocalDateTime>>>()
fun isRateLimited(identifier: String, service: String): Boolean {
val limitConfig = config.limits[service] ?: return false
val currentTime = LocalDateTime.now()
synchronized(requestMap) {
val serviceMap = requestMap.getOrPut(identifier) { ConcurrentHashMap() }
val (requestCount, lastRequestTime) = serviceMap.getOrDefault(service, Pair(0, currentTime))
if (lastRequestTime.plusMinutes(limitConfig.timeFrameMinutes).isBefore(currentTime)) {
serviceMap[service] = Pair(0, currentTime)
return false
}
return requestCount >= limitConfig.limit
}
}
fun registerRequest(identifier: String, service: String) {
val limitConfig = config.limits[service] ?: return
if (isRateLimited(identifier, service)) {
throw RateLimitExceededException(identifier, limitConfig.limit)
}
val currentTime = LocalDateTime.now()
synchronized(requestMap) {
val serviceMap = requestMap.getOrPut(identifier) { ConcurrentHashMap() }
val (requestCount, _) = serviceMap.getOrDefault(service, Pair(0, currentTime))
serviceMap[service] = Pair(requestCount + 1, currentTime)
Timer().schedule(object : TimerTask() {
override fun run() {
synchronized(requestMap) {
requestMap[identifier]?.remove(service)
if (requestMap[identifier]?.isEmpty() == true) {
requestMap.remove(identifier)
}
}
}
}, TimeUnit.MINUTES.toMillis(limitConfig.timeFrameMinutes))
}
}
}
What’s Happening Here?
Tracks requests in ConcurrentHashMap (thread-safe).
If the last request is older than the time limit, it resets the count.
If the limit is exceeded, it throws an exception.
Uses a timer to automatically remove expired entries.
Now, we’ll use Spring AOP to intercept methods with @RateLimited.
First, Add the AOP Dependency
If you haven’t already, add this to build.gradle.kts:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-aop")
}
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.springframework.stereotype.Component
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
@Aspect
@Component
class RateLimiterAspect(private val rateLimiterService: RateLimiterService, private val config: RateLimiterConfig) {
@Around("@annotation(rateLimited)")
fun enforceRateLimit(joinPoint: ProceedingJoinPoint, rateLimited: RateLimited): Any? {
if (!config.enabled) return joinPoint.proceed() // Skip if disabled
val request = (RequestContextHolder.getRequestAttributes() as? ServletRequestAttributes)?.request
val ipAddress = request?.remoteAddr ?: "UNKNOWN"
rateLimiterService.registerRequest(ipAddress, rateLimited.service)
return joinPoint.proceed()
}
}
If a user exceeds the limit, we should return a HTTP 429 Too Many Requests error.
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(RateLimitExceededException::class)
fun handleRateLimitExceeded(ex: RateLimitExceededException) =
ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(ex.message)
}
Let’s say you have a public link shortener service. Without rate limiting, users can:
Flood your API with thousands of shorten requests per second.
Use bots to create spam links.
Crush your database with unlimited writes.
By adding @RateLimited(service = "SHORTEN_URL"), you prevent spam, improve security, and ensure fair access.
🔹 Now your APIs are protected!
🔹 No more hardcoded limits – just update application.properties.
🔹 Easily apply rate limits with @RateLimited annotation.
That’s it for today. Happy coding…