Preventing Duplicate Form Submissions Through Idempotency

Understanding Duplicate Submissions

Normal Submission: When you open a registration page and click the submit button, your data gets sent to the server successfully - this is a standard form submission.

Duplicate Submission: Imagine clicking the registration button, but due to network latency, the page appears frozen. You click again multiple times. Once the network recovers, the server receives duplicate copies of the same data. Without proper handling, this results in multiple accounts being created with identical information.

Potential Consequences: Consider a money transfer scenario. You intend to send $200 to someone, but your network connection is unstable. The transfer button doesn't respond, so you tap it repeatedly. When connectivity restores, you discover that instead of one $200 transfer, the system processed 200 separate transactions - each for $200. This illustrates the critical importance of preventing duplicate submissions.

Preventing duplicate submissions is essentially implementing idempotency: an idempotent operation produces the same result regardless of how many times it executes. In a transfer scenario, clicking the transfer button once should have the same effect as clicking it multiple times - only a single transfer occurs.

Strategies for Preventing Duplicate Submissions

Client-Side Approaches:

  • Button state management: Disable the submit button immediately after the first click until the server responds
  • Page redirection: Navigate to a static page immediately after submission to prevent re-submission
  • Request fingerprinting: Compare incoming request parameters to identify and reject duplicates

Server-Side Approaches:

  • Token-based validation: Verify unique request tokens to ensure each request is processed only once
  • Database constraints: Use unique constraints on critical fields
  • Distributed caching: Store request states in Redis or similar cache systems

Client-Side Implementation

/**
 * Request cache for tracking submitted requests
 * Stores hash fingerprints to prevent duplicate submissions
 */
requestCache: {},

/**
 * Centralized API request handler
 * @param {Object} payload - Data to send to the server
 */
sendRequest(payload) {
    const context = this;
    const deferred = $Defer();
    const cache = context.requestCache;
    
    // Generate unique fingerprint for the request payload
    const requestFingerprint = context.generateFingerprint(payload);
    
    // Check if this exact request is already being processed
    if (cache[requestFingerprint]) {
        deferred.reject({message: "Your request is being processed, please wait..."});
        return deferred.promise;
    }
    
    // Mark request as in-progress
    cache[requestFingerprint] = true;
    
    // Execute asynchronous request
    context.executeAsync(payload).then(function(response) {
        // Clear the cache flag upon completion
        cache[requestFingerprint] = false;
        deferred.resolve(response);
    }, function(error) {
        // Clear the cache flag on failure as well
        cache[requestFingerprint] = false;
        deferred.reject(error);
    });
    
    return deferred.promise;
}

Server-Side Implementation

Token-Based Validation:

The server validates tokens submitted by the client against tokens stored in Redis. If a token has already been processed or is currently being handled, the request is rejected as a duplicate.

public class IdempotencyInterceptor implements MethodInterceptor, ThrowsAdvice {
    private static final Logger logger = LoggerFactory.getLogger(IdempotencyInterceptor.class);
    private static final String PROCESSING_STATUS = "processing";
    
    @Override
    public Object intercept(MethodInvocation invocation) throws Throwable {
        Idempotent annotation = (Idempotent)invocation.getMethod().getAnnotation(Idempotent.class);
        HttpServletRequest httpRequest = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
        
        String requestToken = httpRequest.getHeader("idempotent_token");
        
        if (StringUtils.isEmpty(requestToken)) {
            throw new AccessDeniedException("Request denied: idempotent_token header is required");
        }
        
        String redisKey = "idempotent:token:" + requestToken;
        
        if (!RedisTemplate.hasKey(redisKey)) {
            throw new AccessDeniedException("Request denied: invalid or expired token");
        }
        
        String cachedValue = (String)RedisTemplate.get(redisKey);
        
        if (cachedValue == null) {
            // Attempt to set processing status
            String previousValue = (String)RedisTemplate.getAndSet(redisKey, PROCESSING_STATUS);
            
            if (previousValue != null) {
                // Token was already being processed
                return rejectRequest(redisKey, cachedValue, annotation);
            }
            
            // Execute the actual method
            Object result = invocation.proceed();
            completeRequest(redisKey, result);
            return result;
        } else {
            // Token exists, reject as duplicate
            return rejectRequest(redisKey, cachedValue, annotation);
        }
    }
    
    private Object rejectRequest(String key, String value, Idempotent annotation) {
        logger.warn("Duplicate request detected for token: {}", key);
        // Return appropriate error response
        return null;
    }
    
    private void completeRequest(String key, Object response) {
        RedisTemplate.delete(key);
        logger.info("Request completed and token removed: {}", key);
    }
}

Tags: idempotency duplicate-submission form-validation frontend Backend

Posted on Thu, 29 Oct 2026 16:35:18 +0000 by FoxhoundX