Backend Development 10 min read

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

This article explains why a custom business risk‑control (rate‑limiting) component is needed for AI‑intensive services, compares open‑source solutions, and provides a complete backend implementation using Redis+Lua scripts, Kotlin code, Spring AOP annotations, and real‑time adjustable rules.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Design and Implementation of a Business Rate‑Limiting Component Using Redis Lua Scripts and Custom Annotations in Kotlin

The author, a Java/Kotlin architect, introduces the need for a business‑level risk‑control component because AI services such as OCR and voice evaluation are costly, and the product requires per‑user usage limits.

1. Background

Why risk control? To restrict the number of times expensive AI capabilities are invoked.

Why build our own? Existing open‑source rate‑limiters do not satisfy the specific business requirements, especially the need for combined daily and hourly limits with rollback logic.

Other requirements include real‑time adjustable limits, as the initial thresholds often need to be tuned after deployment.

2. Design Idea

The component must implement three kinds of rules:

Natural‑day count

Natural‑hour count

Combined day‑and‑hour count (with rollback when one condition fails)

After evaluating implementation options (MySQL + transaction, Redis + Lua, distributed transactions), the author chooses Redis+Lua for its simplicity and sufficient accuracy.

3. Concrete Implementation

3.1 Rate‑limit rules

a. Day/Hour rules share a single Lua script because they differ only by the key used:

// 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;

Arguments:

KEYS[1] : the Redis key for the day or hour counter

ARGV[1] : the maximum allowed count

ARGV[2] : expiration time (in milliseconds)

b. Combined day‑and‑hour rule requires a more complex script that updates both counters and rolls back the other when one limit is exceeded:

// lua script (simplified)
local dayValue = 0; local hourValue = 0;
local dayPass = true; local hourPass = true;
-- day logic
local dayCurrent = redis.call('get', KEYS[1]);
if dayCurrent ~= false then
  if tonumber(dayCurrent) < tonumber(ARGV[1]) then
    dayValue = redis.call('INCR', KEYS[1])
  else
    dayPass = false; dayValue = tonumber(dayCurrent) + 1;
  end;
else
  redis.call('set', KEYS[1], 1, 'px', ARGV[3]); dayValue = 1;
end;
-- hour logic (similar)
local hourCurrent = redis.call('get', KEYS[2]);
if hourCurrent ~= false then
  if tonumber(hourCurrent) < tonumber(ARGV[2]) then
    hourValue = redis.call('INCR', KEYS[2])
  else
    hourPass = false; hourValue = tonumber(hourCurrent) + 1;
  end;
else
  redis.call('set', KEYS[2], 1, 'px', ARGV[4]); hourValue = 1;
end;
-- rollback when one fails
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;
return {dayValue, hourValue};

Arguments:

KEYS[1] : day counter key

KEYS[2] : hour counter key

ARGV[1] : day limit

ARGV[2] : hour limit

ARGV[3] : day expiration

ARGV[4] : hour expiration

3.2 Annotation‑based usage

A custom @Detect annotation is defined to declare the event ID and a SpEL expression that extracts the content used for limiting:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class Detect(
    val eventId: String = "",
    val contentSpel: String = ""
)

The corresponding Aspect DetectHandler parses the SpEL expression, builds a context with method arguments (named arg1 , arg2 , …), obtains the content value, and calls detectManager.matchExceptionally(eventId, content) before proceeding with the original method.

@Aspect
@Component
class DetectHandler {
    @Autowired private lateinit var detectManager: DetectManager
    @Resource(name = "detectSpelExpressionParser")
    private lateinit var spelExpressionParser: SpelExpressionParser

    @Bean(name = ["detectSpelExpressionParser"])
    fun detectSpelExpressionParser() = 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!")
        }
        val expression = spelExpressionParser.parseExpression(detect.contentSpel)
        val argMap = joinPoint.args.mapIndexed { index, any -> "arg${index+1}" to any }.toMap()
        val context = StandardEvaluationContext().apply { if (argMap.isNotEmpty()) setVariables(argMap) }
        val content = expression.getValue(context)
        detectManager.matchExceptionally(detect.eventId, content)
        return joinPoint.proceed()
    }
}

3.3 Service usage example

After adding the annotation, the OCR service method becomes:

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

    @Detect(eventId = "ocr", contentSpel = "#arg1")
    override fun submitOcrTask(userId: String, imageUrl: String): String {
        // do ocr
    }
}

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

4. Conclusion

The article demonstrates a practical backend solution for business‑level rate limiting, combining Redis Lua scripts for atomic counters with a Spring‑AOP annotation to keep the business code clean and configurable.

backendRedisKotlinRate LimitingSpring AOPLua
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

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.