Boost Your Spring Boot APIs with a DIY Anti‑Duplicate and Rate‑Limiting Starter

Guardian is a lightweight Spring Boot starter that provides independent anti‑duplicate submission and rate‑limiting modules, offering annotation and YAML configuration, multi‑dimensional key scopes, customizable response modes, Redis or local storage, and built‑in monitoring, all illustrated with step‑by‑step code examples.

SpringMeng
SpringMeng
SpringMeng
Boost Your Spring Boot APIs with a DIY Anti‑Duplicate and Rate‑Limiting Starter

Overview

Guardian is a lightweight Spring Boot starter (v1.3.0) that provides two independent modules: anti‑duplicate submission and API rate limiting. Both modules can be added separately via Maven dependencies and support Redis or local in‑memory storage.

Anti‑duplicate submission

Quick start

Add the starter dependency:

<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-repeat-submit-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>

Annotate the controller method:

@PostMapping("/submit")
@RepeatSubmit(interval = 10, message = "订单正在处理,请勿重复提交")
public Result submitOrder(@RequestBody OrderDTO order) {
    return orderService.submit(order);
}

Run the application; the interceptor becomes active. Within the configured interval (e.g., 10 seconds) a second request with the same user, endpoint and request body is blocked.

Why not use a raw Redis lock?

Key composition : Simple userId+url cannot distinguish different request bodies. Guardian serializes the request body to JSON, encodes it with Base64, and includes it in the lock key.

User identification : When userId is null, Guardian falls back to sessionId and then to client IP, guaranteeing a non‑null key.

Exception handling : If business code throws an exception, the lock would otherwise remain for the full interval. Guardian releases the lock in afterCompletion when an exception occurs.

Selective disabling : A global whitelist ( exclude-urls) with Ant‑style patterns has the highest priority, preventing health‑check or public endpoints from being blocked.

YAML batch configuration

guardian:
  repeat-submit:
    storage: redis
    key-encrypt: md5
    urls:
      - pattern: /api/order/**
        interval: 10
        key-scope: user
        message: "订单正在处理,请勿重复提交"
      - pattern: /api/sms/send
        interval: 60
        key-scope: ip
    exclude-urls:
      - /api/public/**
      - /api/health

YAML rules have higher priority than annotations.

Whitelist entries have the highest priority. key-scope controls the dimension: user, ip or global.

Response handling

guardian:
  repeat-submit:
    response-mode: exception  # default, throws RepeatSubmitException
    # response-mode: json      # directly returns JSON

Exception mode requires a global exception handler:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(RepeatSubmitException.class)
    public Result handleRepeatSubmit(RepeatSubmitException e) {
        return Result.fail(e.getMessage());
    }
}

JSON mode returns a default payload {"code":500,"msg":"...","timestamp":...}. A custom RepeatSubmitResponseHandler bean can override the format.

Running without Redis

Set storage: local to use an in‑memory ConcurrentHashMap with a scheduled cleanup thread. Production environments are still recommended to use Redis for distributed scenarios.

Context‑path handling

Guardian matches both the full request URI (including server.servlet.context-path) and the path stripped of the context‑path, so rules work regardless of how the URI is written in YAML.

Internal flow

Request → RepeatableRequestFilter (caches body) → RepeatSubmitInterceptor
  ├─ Check whitelist → allow if matched
  ├─ Match YAML rules
  ├─ Check @RepeatSubmit annotation → allow if none
  ▼
KeyGenerator (creates key based on scope)
KeyEncrypt (optional MD5)
Storage.tryAcquire()
  ├─ Success → proceed, store key with TTL
  └─ Failure → response-mode handling (exception or JSON)
Business execution
  ├─ Normal → key expires naturally
  └─ Exception → afterCompletion releases lock

API rate limiting

Quick start

<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-rate-limit-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>

Annotation examples:

Sliding window (max 10 QPS): @RateLimit(qps = 10) Token bucket (5 QPS, burst capacity 20):

@RateLimit(qps = 5, capacity = 20, algorithm = RateLimitAlgorithm.TOKEN_BUCKET)

YAML batch configuration

guardian:
  rate-limit:
    urls:
      - pattern: /api/sms/send
        qps: 1
        rate-limit-scope: ip
      - pattern: /api/seckill/**
        qps: 10
        capacity: 50
        algorithm: token_bucket
        rate-limit-scope: global
    exclude-urls:
      - /api/public/**

YAML rules have higher priority than annotations; whitelist has the highest priority.

Sliding window vs. token bucket

Sliding window counts requests within a fixed time window; it strictly enforces the limit (e.g., max 10 requests per second). Suitable for SMS sending or login attempts.

Token bucket refills tokens at a fixed rate; bursts up to the bucket capacity are allowed. Example: 5 QPS, capacity 20 permits a sudden burst of 20 requests, then returns to 5 QPS.

Illustrative comparison (both configured with qps=10 and a burst of 20 requests):

Sliding window: first 10 pass, next 10 rejected, thereafter max 10 per second.

Token bucket: first 10 pass, next 10 also pass (burst), thereafter max 10 per second.

Token refill control

qps=10, window=1s

→ 10 tokens added each second. qps=10, window=1min → 10 tokens added each minute (≈1 token every 6 seconds).

@RateLimit(qps = 10, window = 1, windowUnit = TimeUnit.MINUTES,
        capacity = 10, algorithm = RateLimitAlgorithm.TOKEN_BUCKET)

Rate‑limit dimensions

GLOBAL (default): a single counter for the entire endpoint.

IP : separate counters per client IP (ideal for SMS, captcha).

USER : separate counters per authenticated user.

@RateLimit(qps = 1, rateLimitScope = RateLimitKeyScope.IP,
    message = "短信发送过于频繁")

Response handling

guardian:
  rate-limit:
    response-mode: exception  # default, throws RateLimitException
    # response-mode: json      # directly returns JSON

Custom RateLimitResponseHandler beans can override the JSON format.

Design details

Concurrency safety

Redis: both sliding window and token bucket use Lua scripts, guaranteeing atomic execution.

Local cache: synchronized on a per‑key basis, avoiding cross‑key blocking.

Duplicate‑submission also uses atomic SET NX EX in Redis and atomic methods of ConcurrentHashMap for local storage.

Local cache memory management

A daemon guard thread runs every 5 minutes to purge expired entries, preventing unbounded memory growth.

Pluggable architecture

Core components are defined as interfaces. Spring’s @ConditionalOnMissingBean supplies default implementations, which can be overridden by user‑provided beans.

Component mapping:

Key generation: RepeatSubmitKeyGenerator / RateLimitKeyGenerator Key encryption: AbstractKeyEncrypt Storage: RepeatSubmitStorage / RateLimitStorage Response handling: RepeatSubmitResponseHandler / RateLimitResponseHandler User context (shared):

UserContext

Observability

Logging can be enabled with log-enabled: true to record both blocked and passed requests.

Actuator endpoints expose statistics:

GET /actuator/guardian-repeat-submit   → duplicate‑submission stats
GET /actuator/guardian-rate-limit      → rate‑limit stats
{
  "totalRequestCount": 5560,
  "totalPassCount": 5432,
  "totalBlockCount": 128,
  "blockRate": "2.30%",
  "topBlockedApis": { "/api/sms/send": 56 },
  "topRequestApis": { "/api/search": 3200 }
}

Project structure

guardian-parent
├── guardian-core               # shared utilities
├── guardian-repeat-submit
│   ├── guardian-repeat-submit-core
│   └── guardian-repeat-submit-spring-boot-starter
├── guardian-rate-limit
│   ├── guardian-rate-limit-core
│   └── guardian-rate-limit-spring-boot-starter
├── guardian-storage-redis      # Redis storage shared by both modules
└── guardian-example            # example application

Modules are independent; include only the starter you need.

RedisSpring BootAnnotationYAMLrate limitingduplicate submissionapi-protection
SpringMeng
Written by

SpringMeng

Focused on software development, sharing source code and tutorials for various systems.

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.