How to Implement API Duplicate Prevention and Idempotency in Spring Boot

This article explains how to prevent duplicate API calls and achieve idempotency in Spring Boot 3.0.5 by using unique tokens, timestamps, or Spring AOP, and provides complete code examples for custom annotations, AOP aspects, interceptors, and Redis‑based token management.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Implement API Duplicate Prevention and Idempotency in Spring Boot

What is Interface Duplicate Prevention

Duplicate prevention ensures that an interface request can be executed only once within a certain time window, avoiding repeated submissions that could cause duplicate data or errors.

Methods

Use a unique identifier (e.g., a request token) and reject the request if the identifier has already been consumed.

Employ timestamps or counters to record request time or count and reject repeats within a defined range.

Apply Spring AOP to intercept the request at a specific method or layer and perform duplicate‑check logic.

These techniques help maintain system consistency and stability.

Idempotency vs Duplicate Prevention

Idempotency means that executing an API request multiple times yields the same result, while duplicate prevention focuses on allowing only a single execution within a time frame.

Idempotency Techniques

Include a unique request ID to recognize and block repeated processing.

Use optimistic or pessimistic locking to ensure data consistency.

For update operations, compare old and new data and proceed only when changes are detected.

Duplicate Prevention Techniques

Allow only one execution of an operation or request within a specified period, primarily to avoid repeated data submissions.

Technical Implementation

Method 1: AOP Approach

Custom Annotation

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicate {
    /** Unique identifier key passed via header */
    String header() default "token";
    /** Unique identifier key passed via request parameter */
    String param() default "token";
}

Custom AOP Aspect

@Component
@Aspect
public class PreventDuplicateAspect {
    public static final String PREVENT_PREFIX_KEY = "prevent:";
    private final StringRedisTemplate stringRedisTemplate;
    private final HttpServletRequest request;
    public PreventDuplicateAspect(StringRedisTemplate stringRedisTemplate, HttpServletRequest request) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.request = request;
    }
    @Around("@annotation(prevent)")
    public Object preventDuplicate(ProceedingJoinPoint pjp, PreventDuplicate prevent) throws Throwable {
        String key = prevent.header();
        String value = null;
        if (key != null && key.length() > 0) {
            value = request.getHeader(key);
        } else {
            key = prevent.param();
            if (key != null && key.length() > 0) {
                value = request.getParameter(key);
            }
        }
        if (value == null || "".equals(value.trim())) {
            return "非法请求";
        }
        String prevent_key = PREVENT_PREFIX_KEY + value;
        Boolean result = stringRedisTemplate.delete(prevent_key);
        if (result != null && result.booleanValue()) {
            return pjp.proceed();
        } else {
            return "请不要重复提交";
        }
    }
}

Token Generation API

@RestController
@RequestMapping("/generate")
public class GenerateController {
    private final StringRedisTemplate stringRedisTemplate;
    public GenerateController(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @GetMapping("/token")
    public String token() {
        String token = UUID.randomUUID().toString().replace("-", "");
        stringRedisTemplate.opsForValue().setIfAbsent(
            PreventDuplicateAspect.PREVENT_PREFIX_KEY + token,
            token,
            5 * 60,
            TimeUnit.SECONDS);
        return token;
    }
}

Business API

@RestController
@RequestMapping("/prevent")
public class PreventController {
    @PreventDuplicate
    @GetMapping("/index")
    public Object index() {
        return "index success";
    }
}

Testing steps: first call the token‑generation endpoint to obtain a token, then call the business endpoint with the token. The first call succeeds; subsequent calls are blocked.

Method 2: Interceptor Approach

Custom Interceptor

@Component
public class PreventDuplicateInterceptor implements HandlerInterceptor {
    private final StringRedisTemplate stringRedisTemplate;
    public PreventDuplicateInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod hm) {
            if (hm.hasMethodAnnotation(PreventDuplicate.class)) {
                PreventDuplicate pd = hm.getMethodAnnotation(PreventDuplicate.class);
                String key = pd.header();
                String value = null;
                if (key != null && key.length() > 0) {
                    value = request.getHeader(key);
                } else {
                    key = pd.param();
                    if (key != null && key.length() > 0) {
                        value = request.getParameter(key);
                    }
                }
                if (value == null || "".equals(value.trim())) {
                    response.setContentType("text/plain;charset=utf-8");
                    response.getWriter().println("非法请求");
                    return false;
                }
                String prevent_key = PreventDuplicateAspect.PREVENT_PREFIX_KEY + value;
                Boolean result = stringRedisTemplate.delete(prevent_key);
                if (result != null && result.booleanValue()) {
                    return true;
                } else {
                    response.setContentType("text/plain;charset=utf-8");
                    response.getWriter().println("请不要重复提交");
                    return false;
                }
            }
        }
        return true;
    }
}

Interceptor Configuration

@Component
public class PreventWebConfig implements WebMvcConfigurer {
    private final PreventDuplicateInterceptor duplicateInterceptor;
    public PreventWebConfig(PreventDuplicateInterceptor duplicateInterceptor) {
        this.duplicateInterceptor = duplicateInterceptor;
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.duplicateInterceptor).addPathPatterns("/**");
    }
}

Testing the interceptor follows the same token acquisition and request flow; the first request succeeds, while repeated requests are rejected.

End of tutorial.

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.

AOPRedisSpring BootInterceptoridempotencyDuplicate Prevention
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

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.