Adding Idempotency

In distributed systems and microservice architectures, network fluctuations, timeout retries, and accidental double-clicks from frontend users are practically inevitable. These factors frequently lead to the exact same request being dispatched multiple times. Without proper backend safeguards, such duplicates can trigger severe issues like duplicated database records, double-charging user accounts, and corrupted business states. To conquer this architectural pain point, implementing idempotency becomes an indispensable pillar of backend engineering.

Rooted in mathematics, idempotency originally describes an operation $f$ where $f(x) = f(f(x))$. Translated into computer science, it signifies a vital guarantee: performing an operation multiple times produces the exact same systemic and business outcome as executing it just once.

In practical terms, if an API endpoint is idempotent, no matter how many times a client invokes it, the server's state and the resulting response (at the business logic level) will remain identical after the initial call. For instance, read queries are naturally idempotent by design, whereas state-changing actions like financial transactions or payment processing are non-idempotent by default and must be explicitly engineered to be idempotent.
In real-world software development, several classic and highly effective design patterns are employed to achieve idempotency:

  • Unique Constraints: Leverage database-level unique indexes to block duplicate insertions. For example, when registering a new user, marking the phone number or email address with a unique constraint ensures that even if concurrent requests try to insert identical records, the database will only allow one to succeed while throwing a primary key violation for the rest. The application layer can then catch this exception and gracefully return a success response.
  • State Machine Control: Ideal for workflows involving strict state transitions, such as an order lifecycle (Pending -> Paid -> Shipped). Upon receiving a "payment success" webhook or request, the system first verifies whether the current order state is Pending. If so, it transitions the state to Paid; if the order is already in the Paid state, it treats the request as a duplicate and returns success without re-processing.
  • Anti-Replay Token Mechanism: Before executing a critical submission, the client requests a globally unique token from the server (typically cached in Redis). The client then attaches this token when submitting the form. Upon receipt, the server checks for the token's existence, immediately deletes it to invalidate future uses, and proceeds with the business logic. Any subsequent submission with the same token will be rejected.
  • Distributed Locking: For extreme high-concurrency scenarios, acquiring a unique distributed lock (such as through Redis) based on a business identifier ensures that only the thread holding the lock can execute the operation, while competing threads either wait or abort immediately.

Practical Implementation: Redis and Token Mechanism

Let's examine a concrete Java/Spring Boot code example showcasing the most versatile approach: the Token mechanism.

Step 1: Generating and Issuing the Token

When a user navigates to a checkout page or prepares a form submission, the backend generates a unique token (like a UUID), stores it in Redis, and assigns an appropriate expiration time (e.g., 5 minutes).

@RestController
@RequestMapping("/api/token")
public class TokenController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @GetMapping("/get")
    public String createToken() {
        String token = "token:" + UUID.randomUUID().toString();
        // Store in Redis with a 5-minute TTL
        redisTemplate.opsForValue().set(token, "1", 5, TimeUnit.MINUTES);
        return token;
    }
}

Step 2: Validating and Consuming the Token in Business APIs

When handling core write operations—such as order creation—the API endpoint first validates the presence and validity of the token passed in the request header.

@RestController
@RequestMapping("/api/order")
public class OrderController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @PostMapping("/create")
    public Result createOrder(@RequestHeader("X-Token") String token, @RequestBody OrderDTO orderDTO) {
        if (StringUtils.isEmpty(token)) {
            return Result.fail("Missing anti-replay token");
        }

        // Atomic check-and-delete operation using Redis
        String redisKey = "token:" + token;
        Boolean success = redisTemplate.delete(redisKey);
        
        if (success == null || !success) {
            // Token does not exist or has already been consumed
            return Result.fail("Duplicate submission detected");
        }

        // Proceed with core business logic
        boolean result = orderService.saveOrder(orderDTO);
        if (!result) {
            return Result.fail("Failed to create order");
        }

        return Result.success("Order created successfully");
    }
}

Conclusion

Building robust, highly available systems requires deliberate design choices around idempotency. Developers must carefully select the right strategy based on their specific application profile—whether dealing with read-heavy workloads, extreme concurrency, or strict consistency requirements. In production environments, layering multiple defenses—such as frontend debounce controls, token validation, and database-level constraints—works best to comprehensively safeguard data integrity.