Backend Development 10 min read

Design and Implementation of a Business Rate‑Limiting Component Using Redis Lua Scripts and Kotlin Annotations

This article explains how to design and implement a business-specific rate‑limiting component in Kotlin using Redis Lua scripts and Spring AOP annotations, covering the motivation, rule definitions, counting strategies, Lua script details, annotation creation, and a practical usage example.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Design and Implementation of a Business Rate‑Limiting Component Using Redis Lua Scripts and Kotlin Annotations

1. Background

Why we need rate‑limiting: Our product heavily uses AI capabilities such as OCR and voice evaluation, which are costly in terms of money and resources, so the product layer requires limits on user usage.

Why we build our own component: Existing open‑source rate‑limiting solutions cannot satisfy the specific business requirements, so a custom implementation is necessary.

Other requirements: The limits must be adjustable in real time because most limit values are initially set as fixed numbers but often need to be changed later.

2. Idea

To build a simple business rate‑limiting component we need to implement the following work:

a. Rate‑limiting rules

Daily count (natural day)

Hourly count (natural hour)

Combined daily + hourly count with rollback logic

When the daily check passes but the hourly check fails, both counters must be rolled back; they cannot be counted separately.

b. Choice of counting method

MySQL + transaction : Persistent, traceable, but heavy and complex.

Redis + Lua : Simple to implement; Redis’s ability to execute Lua scripts satisfies transactional needs.

MySQL/Redis + distributed transaction : Requires locking, complex, but provides precise counting.

Because we do not need extremely precise technology and there is no persistence requirement, we choose Redis + Lua .

3. Specific Implementation

3.1 Rate‑limiting rule implementation

a. Daily / Hourly counting (shared Lua script)

The script increments a key if the current value is below the limit; otherwise it returns the current value without incrementing.

//lua script
local currentValue = redis.call('get', KEYS[1]);
if currentValue ~= false then
    if tonumber(currentValue) < tonumber(ARGV[1]) then
        return redis.call('INCR', KEYS[1]);
    else
        return tonumber(currentValue) + 1;
    end
else
    redis.call('set', KEYS[1], 1, 'px', ARGV[2]);
    return 1;
end;

Here KEYS[1] is the key for day or hour, ARGV[1] is the limit, and ARGV[2] is the expiration time.

b. Combined daily + hourly counting

This script handles both counters and performs rollback when one of the checks fails.

//lua script
local dayValue = 0;
local hourValue = 0;
local dayPass = true;
local hourPass = true;
local dayCurrentValue = redis.call('get', KEYS[1]);
if dayCurrentValue ~= false then
    if tonumber(dayCurrentValue) < tonumber(ARGV[1]) then
        dayValue = redis.call('INCR', KEYS[1]);
    else
        dayPass = false;
        dayValue = tonumber(dayCurrentValue) + 1;
    end
else
    redis.call('set', KEYS[1], 1, 'px', ARGV[3]);
    dayValue = 1;
end

local hourCurrentValue = redis.call('get', KEYS[2]);
if hourCurrentValue ~= false then
    if tonumber(hourCurrentValue) < tonumber(ARGV[2]) then
        hourValue = redis.call('INCR', KEYS[2]);
    else
        hourPass = false;
        hourValue = tonumber(hourCurrentValue) + 1;
    end
else
    redis.call('set', KEYS[2], 1, 'px', ARGV[4]);
    hourValue = 1;
end

if (not dayPass) and hourPass then
    hourValue = redis.call('DECR', KEYS[2]);
end
if dayPass and (not hourPass) then
    dayValue = redis.call('DECR', KEYS[1]);
end

local pair = {};
pair[1] = dayValue;
pair[2] = hourValue;
return pair;

In this script KEYS[1] and KEYS[2] are the day and hour keys respectively; ARGV[1] and ARGV[2] are the day and hour limits; ARGV[3] and ARGV[4] are the expiration times.

3.2 Annotation implementation

a. Define @Detect annotation

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class Detect(
    /**
     * Event ID
     */
    val eventId: String = "",
    /**
     * Content expression (SpEL)
     */
    val contentSpel: String = ""
)

b. Define the Aspect handler for @Detect

@Aspect
@Component
class DetectHandler {
    private val logger = LoggerFactory.getLogger(javaClass)

    @Autowired
    private lateinit var detectManager: DetectManager

    @Resource(name = "detectSpelExpressionParser")
    private lateinit var spelExpressionParser: SpelExpressionParser

    @Bean(name = ["detectSpelExpressionParser"])
    fun detectSpelExpressionParser(): SpelExpressionParser {
        return SpelExpressionParser()
    }

    @Around("@annotation(detect)")
    fun operatorAnnotation(joinPoint: ProceedingJoinPoint, detect: Detect): Any? {
        if (detect.eventId.isBlank() || detect.contentSpel.isBlank()) {
            throw illegalArgumentExp("@Detect config is not available!")
        }
        // Parse SpEL expression
        val expression = spelExpressionParser.parseExpression(detect.contentSpel)
        val argMap = joinPoint.args.mapIndexed { index, any -> "arg${index+1}" to any }.toMap()
        // Build evaluation context
        val context = StandardEvaluationContext().apply {
            if (argMap.isNotEmpty()) this.setVariables(argMap)
        }
        // Get content value
        val content = expression.getValue(context)
        // Perform rate‑limit check
        detectManager.matchExceptionally(detect.eventId, content)
        return joinPoint.proceed()
    }
}

4. Testing the Component

4.1 Usage with the annotation

After defining the annotation, a service method can be annotated as follows:

@Service
class OcrServiceImpl : OcrService {
    @Autowired
    private lateinit var detectManager: DetectManager

    /**
     * Submit OCR task with per‑user rate limiting
     */
    @Detect(eventId = "ocr", contentSpel = "#arg1")
    override fun submitOcrTask(userId: String, imageUrl: String): String {
        // do ocr
    }
}

Debugging confirms that the annotation value is retrieved successfully and the SpEL expression is parsed correctly.

5. Reference

Original source: juejin.cn/post/7182774381448282172

6. Promotional Note (non‑technical)

The article ends with an invitation to join a backend‑focused technical community, encouraging developers and recruiters to share job referrals and industry discussions. This part is promotional and not related to the technical content.

backendredisSpringKotlinAnnotationrate limitinglua
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.