Backend Development 17 min read

How to Prevent Duplicate Submissions in Java: Simple Backend Solutions

This article explores practical methods to prevent duplicate submissions in Java applications, starting with a simple front‑end button disabling technique and progressing through several backend strategies—including HashMap, fixed‑size array, double‑checked locking, and Apache Commons LRUMap—complete with code samples and performance considerations.

macrozheng
macrozheng
macrozheng
How to Prevent Duplicate Submissions in Java: Simple Backend Solutions

Problem Statement

A friend once asked: What is the simplest way to prevent duplicate submissions in Java? The key points are preventing duplicate submissions and keeping the solution simple.

Simulating the User Scenario

The scenario is illustrated below:

Sample Spring Boot controller:

<code>import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController {
    /**
     * Method that may be called repeatedly
     */
    @RequestMapping("/add")
    public String addUser(String id) {
        // business logic ...
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}
</code>

Front‑End Interception

Disabling the submit button after a click can block normal duplicate clicks.

Implementation code:

<code>&lt;html&gt;
&lt;script&gt;
    function subCli() {
        // disable button
        document.getElementById("btn_sub").disabled = "disabled";
        document.getElementById("dv1").innerText = "按钮被点击了~";
    }
&lt;/script&gt;
&lt;body style="margin-top: 100px;margin-left: 100px;"&gt;
    &lt;input id="btn_sub" type="button" value=" 提 交 " onclick="subCli()"&gt;
    &lt;div id="dv1" style="margin-top: 80px;"&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code>

However, a malicious user can bypass the front end by sending repeated HTTP requests directly.

Back‑End Interception

The idea is to check whether a request has already been processed before executing the business logic.

Using a

HashMap

to store processed IDs (basic version):

<code>import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

/** Ordinary Map version */
@RequestMapping("/user")
@RestController
public class UserController3 {
    // cache ID set
    private Map<String, Integer> reqCache = new HashMap<>();

    @RequestMapping("/add")
    public String addUser(String id) {
        // non‑null check omitted
        synchronized (this.getClass()) {
            // duplicate request check
            if (reqCache.containsKey(id)) {
                System.out.println("请勿重复提交!!!" + id);
                return "执行失败";
            }
            // store request ID
            reqCache.put(id, 1);
        }
        // business logic ...
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}
</code>

Problem: the

HashMap

grows indefinitely, consuming memory and slowing look‑ups.

Optimized Version – Fixed‑Size Array

Uses a circular array with an index counter to limit size:

<code>import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;

@RequestMapping("/user")
@RestController
public class UserController {
    private static String[] reqCache = new String[100]; // request ID storage
    private static Integer reqCacheCounter = 0; // points to next slot

    @RequestMapping("/add")
    public String addUser(String id) {
        // non‑null check omitted
        synchronized (this.getClass()) {
            // duplicate request check
            if (Arrays.asList(reqCache).contains(id)) {
                System.out.println("请勿重复提交!!!" + id);
                return "执行失败";
            }
            // reset counter if needed
            if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0;
            reqCache[reqCacheCounter] = id;
            reqCacheCounter++;
        }
        // business logic ...
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}
</code>

Version 3 – Double‑Checked Locking (DCL)

Improves performance by separating duplicate check and insertion:

<code>import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;

@RequestMapping("/user")
@RestController
public class UserController {
    private static String[] reqCache = new String[100];
    private static Integer reqCacheCounter = 0;

    @RequestMapping("/add")
    public String addUser(String id) {
        // non‑null check omitted
        // duplicate request check
        if (Arrays.asList(reqCache).contains(id)) {
            System.out.println("请勿重复提交!!!" + id);
            return "执行失败";
        }
        synchronized (this.getClass()) {
            // double‑checked locking
            if (Arrays.asList(reqCache).contains(id)) {
                System.out.println("请勿重复提交!!!" + id);
                return "执行失败";
            }
            if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0;
            reqCache[reqCacheCounter] = id;
            reqCacheCounter++;
        }
        // business logic ...
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}
</code>
Note: DCL is suitable for high‑frequency duplicate‑submission scenarios; otherwise it may not be appropriate.

Version 4 – LRUMap

Apache Commons Collections provides

LRUMap

, which automatically evicts the least‑recently‑used entries:

<code>import org.apache.commons.collections4.map.LRUMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController {
    // max 100 entries, LRU eviction
    private LRUMap<String, Integer> reqCache = new LRUMap<>(100);

    @RequestMapping("/add")
    public String addUser(String id) {
        // non‑null check omitted
        synchronized (this.getClass()) {
            // duplicate request check
            if (reqCache.containsKey(id)) {
                System.out.println("请勿重复提交!!!" + id);
                return "执行失败";
            }
            // store request ID
            reqCache.put(id, 1);
        }
        // business logic ...
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}
</code>

Version 5 – Encapsulated Utility

A reusable utility class centralises the idempotency check:

<code>import org.apache.commons.collections4.map.LRUMap;

/** Idempotency utility */
public class IdempotentUtils {
    // LRU map with capacity 100
    private static LRUMap<String, Integer> reqCache = new LRUMap<>(100);

    /**
     * Idempotency judge
     */
    public static boolean judge(String id, Object lockClass) {
        synchronized (lockClass) {
            if (reqCache.containsKey(id)) {
                System.out.println("请勿重复提交!!!" + id);
                return false;
            }
            reqCache.put(id, 1);
        }
        return true;
    }
}
</code>

Usage in a controller:

<code>import com.example.idempote.util.IdempotentUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController4 {
    @RequestMapping("/add")
    public String addUser(String id) {
        // non‑null check omitted
        // ---- Idempotency check start ----
        if (!IdempotentUtils.judge(id, this.getClass())) {
            return "执行失败";
        }
        // ---- Idempotency check end ----
        // business logic ...
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}
</code>
Tip: For even cleaner code you can create a custom annotation that applies the idempotency check automatically.

Extended Knowledge – LRUMap Implementation Details

The

LRUMap

is built on a circular doubly‑linked list with a header node.

<code>AbstractLinkedMap.LinkEntry entry;
</code>

When a key is accessed, the entry is moved to the most‑recently‑used (MRU) position:

<code>public V get(Object key, boolean updateToMRU) {
    LinkEntry<K, V> entry = this.getEntry(key);
    if (entry == null) {
        return null;
    } else {
        if (updateToMRU) {
            this.moveToMRU(entry);
        }
        return entry.getValue();
    }
}

protected void moveToMRU(LinkEntry<K, V> entry) {
    if (entry.after != this.header) {
        ++this.modCount;
        if (entry.before == null) {
            throw new IllegalStateException("Entry.before is null. This should not occur if your keys are immutable, and you have used synchronization properly.");
        }
        entry.before.after = entry.after;
        entry.after.before = entry.before;
        entry.after = this.header;
        entry.before = this.header.before;
        this.header.before.after = entry;
        this.header.before = entry;
    } else if (entry == this.header) {
        throw new IllegalStateException("Can't move header to MRU This should not occur if your keys are immutable, and you have used synchronization properly.");
    }
}
</code>

When adding a new entry and the map is full, the least‑recently‑used entry (the one after the header) is removed:

<code>protected void addMapping(int hashIndex, int hashCode, K key, V value) {
    // check if container is full
    if (this.isFull()) {
        LinkEntry<K, V> reuse = this.header.after;
        boolean removeLRUEntry = false;
        if (!this.scanUntilRemovable) {
            removeLRUEntry = this.removeLRU(reuse);
        } else {
            while (reuse != this.header && reuse != null) {
                if (this.removeLRU(reuse)) {
                    removeLRUEntry = true;
                    break;
                }
                reuse = reuse.after;
            }
            if (reuse == null) {
                throw new IllegalStateException("Entry.after=null, header.after=" + this.header.after + " header.before=" + this.header.before + " key=" + key + " value=" + value + " size=" + this.size + " maxSize=" + this.maxSize + " This should not occur if your keys are immutable, and you have used synchronization properly.");
            }
        }
        if (removeLRUEntry) {
            if (reuse == null) {
                throw new IllegalStateException("reuse=null, header.after=" + this.header.after + " header.before=" + this.header.before + " key=" + key + " value=" + value + " size=" + this.size + " maxSize=" + this.maxSize + " This should not occur if your keys are immutable, and you have used synchronization properly.");
            }
            this.reuseMapping(reuse, hashIndex, hashCode, key, value);
        } else {
            super.addMapping(hashIndex, hashCode, key, value);
        }
    } else {
        super.addMapping(hashIndex, hashCode, key, value);
    }
}
</code>

The

isFull()

method simply checks whether the current size has reached the maximum capacity:

<code>public boolean isFull() {
    return size >= maxSize;
}
</code>

In summary, the article presents six approaches to prevent duplicate submissions: a front‑end button‑disable method and five back‑end implementations (HashMap, fixed‑size array, DCL array, LRUMap, and a packaged LRUMap utility). The LRUMap version offers automatic eviction of stale entries, making it the most robust solution for high‑traffic idempotent operations.

backendjavaSpring BootidempotencyLRUMap
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

login 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.