Build Your Own Spring Boot API Guard: Anti‑Duplicate Submissions and Rate Limiting

The article introduces Guardian, a lightweight Spring Boot starter that provides independent anti‑duplicate‑submission and rate‑limiting modules, explains why a custom solution is preferable to raw Redis locks, and walks through annotation and YAML configurations, key generation, response handling, storage options, concurrency safety, and observability.

Java Companion
Java Companion
Java Companion
Build Your Own Spring Boot API Guard: Anti‑Duplicate Submissions and Rate Limiting

Overview

Guardian is a lightweight Spring Boot starter that provides two completely independent modules: anti‑duplicate submission and API rate limiting. Each module can be added separately via Maven coordinates

io.github.biggg-guardian:guardian-repeat-submit-spring-boot-starter:1.3.0

or

io.github.biggg-guardian:guardian-rate-limit-spring-boot-starter:1.3.0

. The source code is hosted at https://github.com/BigGG-Guardian/guardian.

Anti‑Duplicate Submission

Quick start

Add the Maven 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 – no additional code is required.

Within the configured interval (e.g., 10 seconds), a second request with the same user, URL, and request parameters is blocked.

Why not a simple Redis SETNX lock?

Key composition – Using only userId+url cannot distinguish different request bodies. Guardian caches the request body with RepeatableRequestFilter, serialises it to JSON, Base64‑encodes it, and includes it in the key.

Unauthenticated users – When userId is null, Guardian falls back to sessionId and then to client IP, guaranteeing a non‑null key.

Exception handling – If business logic throws an exception while the lock is held, Guardian releases the lock in afterCompletion to avoid false “duplicate submission” errors.

Selective disabling – A whitelist defined by exclude-urls (Ant‑style patterns) has the highest priority and bypasses all anti‑duplicate logic.

YAML bulk 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
key-scope

controls the protection dimension: user (per user), ip (per IP), or global (shared).

Response modes

exception (default) – Throws RepeatSubmitException for a global exception handler.

json – Returns a JSON payload directly, e.g. {"code":500,"msg":"...","timestamp":...}.

Custom response handling can be provided by defining a RepeatSubmitResponseHandler bean.

Running without Redis

Set storage: local to use an in‑memory ConcurrentHashMap with a daemon thread that cleans expired keys every five minutes.

Context‑path compatibility

When server.servlet.context-path is set, Guardian matches both the full URI and the URI stripped of the context path, so rules work regardless of the prefix.

Internal processing flow

Request → RepeatableRequestFilter (caches body) → RepeatSubmitInterceptor
  ├─ whitelist check (pass if matched)
  ├─ YAML rule match
  ├─ @RepeatSubmit annotation check (pass if none matched)
  ▼
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

Motivation

Duplicate‑submission protection stops identical requests in a short window but cannot limit high‑frequency calls with different parameters (e.g., a script sending 1000 searches per second). Rate limiting controls request frequency at the API layer.

Quick start

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

Supported algorithms:

Sliding window – strict QPS limit.

Token bucket – allows bursts.

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 bulk configuration

guardian:
  rate-limit:
    urls:
      - pattern: /api/sms/send
        qps: 1
        rate-limit-scope: ip
        message: "短信发送过于频繁"
      - 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 rules have the highest priority.

Sliding window vs. token bucket

Sliding window counts requests within a fixed time window; excess requests are rejected. Guarantees that the request count never exceeds the configured QPS (e.g., qps=10, window=1s).

Token bucket refills tokens at a fixed rate; accumulated tokens allow a burst up to the bucket capacity. Example: qps=5, capacity=20 permits up to 20 instantaneous requests, then continues at 5 QPS.

Illustrative comparison (qps = 10, burst = 20): first 10 requests pass for both algorithms; requests 11‑20 are rejected by sliding window but pass by token bucket; subsequent seconds allow up to 10 requests per second for both.

Controlling token refill rate

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‑limiting dimensions

GLOBAL (default) – a single counter for the entire API (e.g., site‑wide search).

IP – independent counters per client IP (e.g., SMS, captcha).

USER – independent counters per authenticated user (e.g., user‑action limits).

Response handling

exception (default) – Throws RateLimitException for a global handler.

json – Returns a JSON response directly.

Custom handlers can be supplied by defining a RateLimitResponseHandler bean.

Concurrency safety

Redis – Both algorithms use Lua scripts; Redis executes Lua atomically.

Local cache – Uses synchronized blocks at key granularity.

Anti‑duplicate submission also relies on atomic SET NX EX in Redis and atomic methods of ConcurrentHashMap for local storage.

Pluggable architecture

Key generator: RepeatSubmitKeyGenerator / RateLimitKeyGenerator Key encrypt: AbstractKeyEncrypt (shared)

Storage: RepeatSubmitStorage / RateLimitStorage Response handler: RepeatSubmitResponseHandler / RateLimitResponseHandler User context: UserContext (shared)

Observability

Intercept logs can be enabled with log-enabled: true.

Actuator endpoints:

GET /actuator/guardian-repeat-submit   → anti‑duplicate statistics
GET /actuator/guardian-rate-limit      → rate‑limit statistics

Sample rate‑limit statistics JSON includes total request count, pass count, block count, block rate, top blocked APIs, and top requested APIs.

Project structure

guardian-parent
├── guardian-core                 # shared utilities (UserContext, etc.)
├── 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 implementation shared by both modules
└── guardian-example              # Example application

Modules are independent; include only the starter(s) you need.

Conclusion

Anti‑duplicate submission – Blocks rapid repeat requests, supports annotation and YAML configuration, multiple key scopes (user, IP, global), automatic lock release on exceptions, and context‑path‑aware matching.

API rate limiting – Controls request frequency with sliding‑window or token‑bucket algorithms, supports burst handling, and three scopes (global, IP, user).

Both modules default to Redis storage but can fall back to local in‑memory storage, expose monitoring via Actuator, and allow custom beans for key generation, encryption, storage, and response handling. They provide lightweight protection for Spring Boot applications without requiring heavyweight gateway solutions.

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.

RedisSpring BootAnnotationrate limitingYAML configurationAnti-duplicate SubmissionAPI Guard
Java Companion
Written by

Java Companion

A highly professional Java public account

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.