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.
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: 203. 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
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.
