Backend Development 17 min read

Preventing Duplicate Submissions in Java: Front‑end and Back‑end Solutions with HashMap, Fixed Array, DCL and LRUMap

This article explains how to prevent duplicate form submissions in a single‑node Java Spring Boot application by first simulating the user scenario, then showing front‑end button disabling, and finally presenting five back‑end idempotency implementations ranging from a simple HashMap to a reusable LRUMap‑based utility.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Preventing Duplicate Submissions in Java: Front‑end and Back‑end Solutions with HashMap, Fixed Array, DCL and LRUMap

Simulated User Scenario

A friend asked the author how to prevent duplicate submissions in Java with the simplest solution; the environment is a single‑node deployment, so the author reproduces the problem using a Spring Boot controller.

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("Adding user ID:" + id);
        return "Success!";
    }
}

Front‑end Interception

Front‑end interception disables or hides the submit button after a click, preventing normal users from sending the request twice.

Implementation code:

<html>
<script>
    function subCli() {
        // disable button
        document.getElementById("btn_sub").disabled = "disabled";
        document.getElementById("dv1").innerText = "Button clicked~";
    }
</script>
<body style="margin-top: 100px; margin-left: 100px;">
    <input id="btn_sub" type="button" value="  提 交 " onclick="subCli()">
    <div id="dv1" style="margin-top: 80px;"></div>
</body>
</html>

However, a malicious client can bypass the UI and send repeated HTTP requests directly, so back‑end protection is also required.

Back‑end Interception

The back‑end approach checks whether a business ID has already been processed before executing the method. The author demonstrates five versions, each improving memory usage and concurrency safety.

1. Basic Version – HashMap

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 of request IDs
    private Map
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("Please do not submit repeatedly!!!" + id);
                return "Failure";
            }
            // store request ID
            reqCache.put(id, 1);
        }
        // business logic ...
        System.out.println("Adding user ID:" + id);
        return "Success!";
    }
}

This implementation suffers from unbounded growth of the HashMap, which eventually consumes too much memory and slows look‑ups.

2. Optimized Version – Fixed‑size Array

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 the 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("Please do not submit repeatedly!!!" + id);
                return "Failure";
            }
            // record request ID
            if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0; // reset counter
            reqCache[reqCacheCounter] = id;
            reqCacheCounter++;
        }
        // business logic ...
        System.out.println("Adding user ID:" + id);
        return "Success!";
    }
}

3. Extended Version – Double‑Checked Locking (DCL)

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 (fast path)
        if (Arrays.asList(reqCache).contains(id)) {
            System.out.println("Please do not submit repeatedly!!!" + id);
            return "Failure";
        }
        synchronized (this.getClass()) {
            // double‑checked locking to avoid unnecessary synchronization
            if (Arrays.asList(reqCache).contains(id)) {
                System.out.println("Please do not submit repeatedly!!!" + id);
                return "Failure";
            }
            // record request ID
            if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0;
            reqCache[reqCacheCounter] = id;
            reqCacheCounter++;
        }
        // business logic ...
        System.out.println("Adding user ID:" + id);
        return "Success!";
    }
}
Note: DCL is suitable for scenarios with a high frequency of duplicate submissions; it is not appropriate for low‑traffic use cases.

4. Complete Version – LRUMap

Apache Commons Collections provides LRUMap , which automatically evicts the least‑recently‑used entries when a fixed capacity is reached.

org.apache.commons
commons-collections4
4.4
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 {
    // LRUMap with a maximum of 100 entries
    private LRUMap
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("Please do not submit repeatedly!!!" + id);
                return "Failure";
            }
            // store request ID
            reqCache.put(id, 1);
        }
        // business logic ...
        System.out.println("Adding user ID:" + id);
        return "Success!";
    }
}

Using LRUMap makes the code much cleaner and guarantees bounded memory usage.

5. Final Version – Encapsulation

import org.apache.commons.collections4.map.LRUMap;

/** Idempotency utility */
public class IdempotentUtils {
    // LRUMap with capacity 100, automatically evicts old entries
    private static LRUMap
reqCache = new LRUMap<>(100);

    /**
     * Returns true if the request is not a duplicate; otherwise false.
     */
    public static boolean judge(String id, Object lockClass) {
        synchronized (lockClass) {
            if (reqCache.containsKey(id)) {
                System.out.println("Please do not submit repeatedly!!!" + id);
                return false;
            }
            reqCache.put(id, 1);
        }
        return true;
    }
}
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 "Failure";
        }
        // ---- idempotency check end ----
        // business logic ...
        System.out.println("Adding user ID:" + id);
        return "Success!";
    }
}

Extended Knowledge – LRUMap Implementation Analysis

LRUMap is built on a circular doubly‑linked list with a sentinel header node. When an entry is accessed, it is moved to the position just before the header (most‑recently‑used). When a new entry is added and the map is full, the entry after the header (least‑recently‑used) is evicted.

AbstractLinkedMap.LinkEntry entry;

Access method (simplified):

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

protected void moveToMRU(LinkEntry
entry) {
    if (entry.after != this.header) {
        // unlink entry
        entry.before.after = entry.after;
        entry.after.before = entry.before;
        // insert before header (MRU position)
        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");
    }
}

Insertion when the map is full:

protected void addMapping(int hashIndex, int hashCode, K key, V value) {
    if (this.isFull()) {
        LinkEntry
reuse = this.header.after; // LRU entry
        if (!this.scanUntilRemovable) {
            this.removeLRU(reuse);
        } else {
            while (reuse != this.header && reuse != null) {
                if (this.removeLRU(reuse)) {
                    break;
                }
                reuse = reuse.after;
            }
            if (reuse == null) {
                throw new IllegalStateException("Entry.after=null");
            }
        }
        this.reuseMapping(reuse, hashIndex, hashCode, key, value);
    } else {
        super.addMapping(hashIndex, hashCode, key, value);
    }
}

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

In summary, LRUMap maintains a circular doubly‑linked list; accessed entries are moved to the MRU position (just before the header), and when the capacity is exceeded the LRU entry (the node after the header) is removed.

Conclusion

The article presented six ways to prevent duplicate submissions: a front‑end button‑disable technique and five back‑end strategies—HashMap, fixed‑size array, double‑checked locking, LRUMap, and a reusable utility class. All solutions are suitable for single‑node environments; distributed scenarios would require Redis or a database.

backendJavaSpring BootIdempotencyduplicate submissionLRUMap
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

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.