Mastering API Idempotency: Strategies, Code Samples, and Best Practices

This article explains the concept of idempotency, why it matters for HTTP APIs, compares the idempotent characteristics of RESTful methods, and presents four practical implementation strategies—including database primary keys, optimistic locking, Redis‑based tokens, and downstream sequence numbers—along with complete Spring Boot code examples and testing guidance.

IT Architects Alliance
IT Architects Alliance
IT Architects Alliance
Mastering API Idempotency: Strategies, Code Samples, and Best Practices

What Is Idempotency?

In mathematics an operation is idempotent when applying it twice yields the same result as applying it once. In programming, an idempotent operation produces identical effects no matter how many times it is executed with the same parameters, ensuring that repeated calls do not change system state.

Idempotency in HTTP/1.1

The HTTP/1.1 specification defines idempotency as the property that multiple identical requests should have the same effect as a single request, ignoring network time‑outs. The first request may cause side effects, but subsequent retries must not create additional side effects.

Why Implement Idempotency?

Front‑end users may click a submit button repeatedly when a response is delayed.

Malicious actors can perform repeated actions such as voting or ordering.

HTTP clients often enable automatic retry on timeout, causing duplicate submissions.

Message queues may deliver the same message more than once.

Idempotency eliminates these problems by guaranteeing that repeated executions are harmless.

Impact on the System

Parallel operations may need to be serialized, reducing throughput.

Additional business logic is required to enforce idempotency, increasing code complexity.

Therefore, the decision to add idempotency should be based on concrete business requirements.

Idempotency of RESTful HTTP Methods

GET : always idempotent – it only retrieves resources.

POST : generally non‑idempotent – each call creates a new resource.

PUT : conditionally idempotent – updating a resource with the same payload can be idempotent, but cumulative updates are not.

DELETE : conditionally idempotent – deleting the same resource repeatedly has no further effect, but bulk deletes with query criteria may not be idempotent.

Implementation Strategies

1. Database Unique Primary Key

Use the unique‑constraint of a primary key to guarantee that an insert operation can only succeed once. This works well for operations that naturally have a unique identifier (e.g., order number). The primary key should be a globally unique ID (such as a distributed ID) rather than an auto‑increment column.

Applicable operations : insert, delete.

Limitation : requires a globally unique key and is only suitable for insert‑type actions.

2. Database Optimistic Lock

Add a version column to the table. Each update includes the current version value in the WHERE clause; the update succeeds only if the version matches, otherwise it fails. This prevents lost updates caused by concurrent modifications.

Applicable operation : update.

Limitation : requires an extra version field in the table.

Example SQL:

UPDATE my_table SET price = price + 50, version = version + 1 WHERE id = 1 AND version = 5;

If the row’s version is no longer 5, the statement affects zero rows, making the operation idempotent.

3. Anti‑Replay Token (Redis)

Generate a token (e.g., UUID) on the client, store it in Redis with a short TTL, and require the token to be sent with the request (typically in a header). The server atomically checks the token and deletes it using a Lua script; if the token is missing or mismatched, the request is rejected as a duplicate.

Applicable operations : insert, update, delete.

Limitation : needs a globally unique token and a Redis instance.

Token generation (simplified):

String token = UUID.randomUUID().toString();
String key = "idempotent_token:" + token;
redisTemplate.opsForValue().set(key, userInfo, 5, TimeUnit.MINUTES);
return token;

Token validation (Lua script):

String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(key, userInfo));
return result != null && result != 0L;

4. Downstream Sequence Number

The downstream service generates a short‑lived unique sequence number (or order ID) and includes it in the request to the upstream service. The upstream service composes a Redis key from the sequence number and a downstream authentication ID. If the key already exists, the request is a duplicate; otherwise the key is stored and the business logic proceeds.

Applicable operations : insert, update, delete.

Limitation : requires the downstream to generate a unique sequence and Redis for storage.

Practical Example (Spring Boot)

1. Maven Dependencies

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
    </parent>
    <groupId>mydlq.club</groupId>
    <artifactId>springboot-idempotent-token</artifactId>
    <version>0.0.1</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

2. Redis Configuration (application.yml)

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    timeout: 1000
    lettuce:
      pool:
        max-active: 100
        max-wait: -1
        min-idle: 0
        max-idle: 20

3. Token Utility Service

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class TokenUtilService {
    private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";
    @Autowired
    private StringRedisTemplate redisTemplate;

    /** Generate a token and store it in Redis for 5 minutes */
    public String generateToken(String value) {
        String token = UUID.randomUUID().toString();
        String key = IDEMPOTENT_TOKEN_PREFIX + token;
        redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
        return token;
    }

    /** Validate token atomically using a Lua script */
    public boolean validToken(String token, String value) {
        String key = IDEMPOTENT_TOKEN_PREFIX + token;
        String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
        Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(key, value));
        if (result != null && result != 0L) {
            log.info("Token validation succeeded: token={}, key={}, value={}", token, key, value);
            return true;
        }
        log.info("Token validation failed: token={}, key={}, value={}", token, key, value);
        return false;
    }
}

4. Controller for Demonstration

import lombok.extern.slf4j.Slf4j;
import mydlq.club.example.service.TokenUtilService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
public class TokenController {
    @Autowired
    private TokenUtilService tokenService;

    @GetMapping("/token")
    public String getToken() {
        String userInfo = "mydlq"; // simulated user info
        return tokenService.generateToken(userInfo);
    }

    @PostMapping("/test")
    public String test(@RequestHeader("token") String token) {
        String userInfo = "mydlq";
        boolean ok = tokenService.validToken(token, userInfo);
        return ok ? "正常调用" : "重复调用";
    }
}

5. JUnit Test to Verify Idempotency

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class IdempotenceTest {
    @Autowired
    private WebApplicationContext webApplicationContext;

    @Test
    public void interfaceIdempotenceTest() throws Exception {
        MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
        String token = mockMvc.perform(MockMvcRequestBuilders.get("/token").accept(MediaType.TEXT_HTML))
                .andReturn().getResponse().getContentAsString();
        for (int i = 1; i <= 5; i++) {
            String result = mockMvc.perform(MockMvcRequestBuilders.post("/test")
                    .header("token", token)
                    .accept(MediaType.TEXT_HTML))
                    .andReturn().getResponse().getContentAsString();
            if (i == 1) {
                Assert.assertEquals("正常调用", result);
            } else {
                Assert.assertEquals("重复调用", result);
            }
        }
    }
}

Conclusion

Idempotency is essential for reliable services, especially those involving financial transactions or order processing. Choose the implementation that matches the business scenario:

Use a unique primary key for insert‑only flows.

Apply optimistic locking for update‑heavy operations.

Adopt downstream sequence numbers when the caller can generate a globally unique identifier.

Employ a Redis‑backed token for generic duplicate‑submission protection.

Understanding the trade‑offs—complexity, performance impact, and storage requirements—helps design robust APIs that behave predictably under retries and concurrent access.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaRedisAPIIdempotencySpringBoot
IT Architects Alliance
Written by

IT Architects Alliance

Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.

0 followers
Reader feedback

How this landed with the community

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.