How to Ensure Idempotent Requests in Java Backend: Strategies and Code Samples

This article explains the concept of idempotency in programming, outlines common causes of duplicate submissions, and presents multiple solutions—including frontend button disabling, PRG pattern, session tokens, database constraints, local locks using ConcurrentHashMap, AOP interceptors, and distributed Redis locks—complete with Java code examples.

Java Backend Technology
Java Backend Technology
Java Backend Technology
How to Ensure Idempotent Requests in Java Backend: Strategies and Code Samples

1. What is Idempotency

In programming, an operation is idempotent when executing it multiple times has the same effect as executing it once. Typical idempotent operations include SELECT queries, DELETE (deleting the same record repeatedly), and UPDATE that sets a fixed value. Non‑idempotent operations include UPDATE that increments a value and INSERT, which creates a new row each time.

2. Causes of Duplicate Submissions

Clicking the submit button twice

Clicking the browser refresh button

Using the back button to repeat a previous request

Repeating a request from browser history

Browser sending duplicate HTTP requests

NGINX retrying a request

Distributed RPC frameworks retrying calls

3. Solutions

Disable the submit button on the frontend using JavaScript components.

Apply the Post/Redirect/Get (PRG) pattern to avoid resubmission on page refresh.

Store a unique token in the session and compare it with a hidden field in the form.

Use cache‑control headers to prevent browser caching of POST requests (not suitable for mobile apps).

Leverage database constraints such as unique indexes or optimistic locking (version field).

Use pessimistic locking with SELECT … FOR UPDATE (caution: may cause deadlocks).

Implement a local lock using ConcurrentHashMap and a scheduled task to release the lock after a delay.

Apply AOP interceptors to centralize duplicate‑submission checks.

Use a distributed Redis lock for clustered environments.

Local Lock Implementation (Java)

import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Resubmit {
    /** Delay in seconds before the lock can be acquired again */
    int delaySeconds() default 20;
}
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.Objects;
import java.util.concurrent.*;

@Slf4j
public final class ResubmitLock {
    private static final ConcurrentHashMap<String, Object> LOCK_CACHE = new ConcurrentHashMap<>(200);
    private static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5, new ThreadPoolExecutor.DiscardPolicy());

    private ResubmitLock() {}

    private static class SingletonInstance {
        private static final ResubmitLock INSTANCE = new ResubmitLock();
    }
    public static ResubmitLock getInstance() { return SingletonInstance.INSTANCE; }

    public static String handleKey(String param) {
        return DigestUtils.md5Hex(param == null ? "" : param);
    }

    public boolean lock(final String key, Object value) {
        return Objects.isNull(LOCK_CACHE.putIfAbsent(key, value));
    }

    public void unLock(final boolean lock, final String key, final int delaySeconds) {
        if (lock) {
            EXECUTOR.schedule(() -> LOCK_CACHE.remove(key), delaySeconds, TimeUnit.SECONDS);
        }
    }
}

AOP Interceptor

import com.alibaba.fastjson.JSONObject;
import com.cn.xxx.common.annotation.Resubmit;
import com.cn.xxx.common.annotation.impl.ResubmitLock;
import com.cn.xxx.common.dto.RequestDTO;
import com.cn.xxx.common.dto.ResponseDTO;
import com.cn.xxx.common.enums.ResponseCode;
import lombok.extern.log4j.Log4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

@Log4j
@Aspect
@Component
public class ResubmitDataAspect {
    private static final String DATA = "data";
    private static final Object PRESENT = new Object();

    @Around("@annotation(com.cn.xxx.common.annotation.Resubmit)")
    public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        Resubmit annotation = method.getAnnotation(Resubmit.class);
        int delaySeconds = annotation.delaySeconds();
        Object[] args = joinPoint.getArgs();
        String key = "";
        Object firstParam = args[0];
        if (firstParam instanceof RequestDTO) {
            JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());
            JSONObject data = JSONObject.parseObject(requestDTO.getString(DATA));
            if (data != null) {
                StringBuilder sb = new StringBuilder();
                data.forEach((k, v) -> sb.append(v));
                key = ResubmitLock.handleKey(sb.toString());
            }
        }
        boolean lock = false;
        try {
            lock = ResubmitLock.getInstance().lock(key, PRESENT);
            if (lock) {
                return joinPoint.proceed();
            } else {
                return new ResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);
            }
        } finally {
            ResubmitLock.getInstance().unLock(lock, key, delaySeconds);
        }
    }
}

Redis Distributed Lock (Spring Boot)

package com.battcn.interceptor;

import com.battcn.annotation.CacheLock;
import com.battcn.utils.RedisLockHelper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.util.UUID;

@Aspect
@Configuration
public class LockMethodInterceptor {
    private final RedisLockHelper redisLockHelper;
    private final CacheKeyGenerator cacheKeyGenerator;

    @Autowired
    public LockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator) {
        this.redisLockHelper = redisLockHelper;
        this.cacheKeyGenerator = cacheKeyGenerator;
    }

    @Around("execution(public * *(..)) && @annotation(com.battcn.annotation.CacheLock)")
    public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        CacheLock lock = method.getAnnotation(CacheLock.class);
        if (lock.prefix().isEmpty()) {
            throw new RuntimeException("lock key can't be null");
        }
        String lockKey = cacheKeyGenerator.getLockKey(pjp);
        String value = UUID.randomUUID().toString();
        boolean success = false;
        try {
            success = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());
            if (!success) {
                throw new RuntimeException("duplicate submission");
            }
            return pjp.proceed();
        } finally {
            redisLockHelper.unlock(lockKey, value);
        }
    }
}

The article concludes that using a local lock based on ConcurrentHashMap and Content‑MD5 hashing works well for single‑node deployments, while a Redis‑backed distributed lock is recommended for clustered environments.

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.

spring-bootdistributed-lock
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

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.