Introduction: Why Do Users Keep Spamming Your Buttons?
Picture this: A user hammers the “Submit Order” button like a kid playing an arcade game—only to trigger 5 identical orders in your system. Next thing you know, customer service lines are blowing up, your database is cluttered with junk data, and you’re woken up at 2 AM to roll back the system. That’s the “horror story” of duplicate submissions.
Duplicate submissions are essentially a chemical reaction of network latency + user impatience. Preventing them isn’t just a technical need—it’s a humanitarian effort to save programmers’sleep! This article will walk you through building a “bronze-to-king” anti-duplicate system with Spring Boot, using a progressive approach from local locks to distributed locks.
1. Basic Approach: Local Lock Annotation (Single-Server Setup)
1.1 Implement a Custom Annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {int lockTime() default 3; // Lock lasts 3s by default; should be longer than frontend button disable time
}
1.2 Implement Lock Logic with AOP
@Aspect
@Component
public class RepeatSubmitAspect {// Use ConcurrentHashMap as local lock storage (NOTE: Fails in distributed environments!)
private static final ConcurrentHashMap<String, Object> LOCKS = new ConcurrentHashMap<>();
@Around("@annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint point, NoRepeatSubmit noRepeatSubmit) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
// Generate a unique lock key: User ID + API path (use more complex rules in production)
String lockKey = getUserId(request) + "-" + request.getServletPath();
// If lock already exists, throw an exception (custom business exceptions recommended)
if (LOCKS.putIfAbsent(lockKey, Boolean.TRUE) != null) {throw new RuntimeException("Too fast! Slow down a bit."); // User-friendly message
}
try {return point.proceed(); // Execute the actual business method
} finally {// Release lock with delay (ALWAYS use finally to ensure release!)
Thread.sleep(noRepeatSubmit.lockTime() * 1000);
LOCKS.remove(lockKey);
}
}
// In production, parse User ID from token; simplified demo here
private String getUserId(HttpServletRequest request) {return Optional.ofNullable(request.getHeader("Authorization"))
.orElse("anonymous");
}
}
1.3 Usage Example: Order Creation API
@RestController
public class OrderController {@NoRepeatSubmit(lockTime = 5) // Block duplicate calls for 5 seconds
@PostMapping("/createOrder")
public ResponseEntity createOrder(@RequestBody OrderDTO order) {// Actual order creation logic (usually requires transaction management)
orderService.create(order);
return ResponseEntity.ok("Order created successfully");
}
}
⚠️ Rant Alert:This works for 90% of duplicate submission cases in single-server setups—but it completely breaks in clustered deployments. Why? Because each server’s is isolated, so locks on one server won’t block requests to other servers. Let’s fix that with a distributed solution!
Top comments (0)