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.
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.
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.