# Configurable Rate Limiter in Spring Boot

## **Why Do We Need Rate Limiting?**

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

## **What We’re Building**

We’ll create a **configurable rate limiter** where:  
✅ You define limits in [`application.properties`](http://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

## **1\. Define Rate Limits in** [`application.properties`](http://application.properties)

Instead of hardcoding limits in the code, we’ll define them in [`application.properties`](http://application.properties):

```plaintext
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.
    

## **2\. Load Configurations in a Kotlin Class**

Spring Boot provides a way to read these values using `@ConfigurationProperties`:

```kotlin
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.
    

## **3\. Create a** `@RateLimited` Annotation

To make things easy, let’s create an annotation that we can apply to API methods:

```kotlin
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class RateLimited(val service: String)
```

This allows us to do things like:

```kotlin
@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.

## **4\. Build the Rate Limiter Logic**

We need a service that will track API requests **per IP address and per service type**.

```kotlin
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**.
    

## **5\. Enforce Rate Limits with Spring AOP**

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`:

```kotlin
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-aop")
}
```

### **Now, Create the AOP Aspect**

```kotlin
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()
    }
}
```

## **6\. Handle Errors Gracefully**

If a user exceeds the limit, we should return a **HTTP 429 Too Many Requests** error.

```kotlin
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)
}
```

### **Real-World Example**

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.**

### **Learning**

🔹 **Now your APIs are protected!**  
🔹 **No more hardcoded limits** – just update [`application.properties`](http://application.properties).  
🔹 **Easily apply rate limits with** `@RateLimited` annotation.

---

That’s it for today. Happy coding…

%%[buymeacoffee-donate] 

%%[ylnk]
