Implementing Business Rate Limiting with Redis, Lua, and Kotlin Annotations

This article explains why a custom business rate‑limiting component is needed, outlines the required rules, chooses Redis + Lua for counting, and provides complete Kotlin/Spring code—including a Detect annotation, Lua scripts, and usage examples—to enforce daily, hourly, and combined limits with real‑time adjustments.

Top Architect
Top Architect
Top Architect
Implementing Business Rate Limiting with Redis, Lua, and Kotlin Annotations

The author, a senior architect, introduces a business risk‑control (rate‑limiting) feature needed for AI‑heavy services such as OCR and voice evaluation, which consume significant resources and therefore require usage limits per user.

Background

Why risk control? To restrict costly AI calls and protect resources.

Why build a custom solution? Existing open‑source rate‑limiters cannot satisfy the specific business requirements, such as combined daily and hourly limits with rollback logic.

Other requirements include real‑time adjustable limits.

Design Idea

The component will implement three rule types:

Natural‑day count

Natural‑hour count

Combined day + hour count with rollback

After evaluating options (MySQL + transaction, Redis + Lua, distributed transactions), the author selects redis+lua for its simplicity and sufficient precision.

Rule Implementation

Lua scripts are used for counting. The first script handles simple daily or hourly limits:

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

The second script handles the combined day + hour rule with rollback:

-- lua script for day+hour with rollback
local dayValue = 0;
local hourValue = 0;
local dayPass = true;
local hourPass = true;
-- day logic
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;
-- hour logic (similar)
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;
-- rollback handling
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};

Invocation Method

A generic entry point DetectManager.matchExceptionally(eventId, content) is defined to invoke the rule matching and throw an exception when the limit is exceeded.

@Component
class DetectManager {
    fun matchExceptionally(eventId: String, content: String) {
        val rt = ruleService.match(eventId, content)
        if (!rt) {
            throw BaseException(ErrorCode.OPERATION_TOO_FREQUENT)
        }
    }
}

Service methods call this manager before performing the actual business logic.

Annotation‑Based Approach

An annotation @Detect is introduced to declare the event ID and a SpEL expression for the content, allowing declarative rate‑limit enforcement.

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

The corresponding aspect parses the SpEL expression, builds a context with method arguments ( arg1, arg2, …), obtains the content, and calls DetectManager.matchExceptionally.

@Aspect
@Component
class DetectHandler {
    @Autowired
    private lateinit var detectManager: DetectManager
    @Resource(name = "detectSpelExpressionParser")
    private lateinit var spelExpressionParser: 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()
    }
}

Usage example in an OCR service:

@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
        return "result"
    }
}

Testing

After applying the annotation, the system successfully retrieves the annotation value, parses the SpEL expression, and enforces the limit.

Conclusion

The article provides a complete, practical guide to building a customizable business rate‑limiting component using Redis + Lua for counting and Kotlin/Spring annotations for declarative usage, satisfying real‑time adjustability and combined day‑hour constraints.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

redisKotlinannotationsrate limitingLua
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

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.