Implementing API Idempotency in Spring Boot Using Tokens, Database Primary Keys, Optimistic Locks, and Redis
This article explains the concept of idempotency in computing and HTTP, why it is essential for APIs, the impact on system design, which RESTful methods are idempotent, and presents four practical implementation strategies—including database primary keys, optimistic locking, anti‑repeat tokens, and downstream sequence numbers—accompanied by complete Spring Boot code examples and testing procedures.
1. What Is Idempotency
Idempotency is a mathematical and computing concept where applying an operation multiple times yields the same result as applying it once.
2. HTTP Idempotency
In HTTP/1.1, an idempotent request produces the same effect on a resource no matter how many times it is repeated, except for network time‑outs.
3. Why Idempotency Is Needed
It prevents duplicate submissions caused by front‑end repeated clicks, malicious repeated actions, client retries, or message re‑consumption, thereby simplifying client logic and protecting system state.
4. Impact on the System
Introducing idempotency can increase server‑side complexity: parallel operations may need to be serialized, additional business logic is required, and extra storage (e.g., Redis) is often used.
5. Idempotency of RESTful Methods
Method
Idempotent?
Description
GET
✓
Retrieves resources without side effects.
POST
✗
Creates new resources; each call adds data.
PUT
–
May be idempotent if it overwrites a fixed value; not if it performs accumulative updates.
DELETE
–
Idempotent when deleting by unique identifier; not when deleting by condition.
6. How to Implement Idempotency
The article presents four practical schemes:
Database Unique Primary Key : Use a globally unique primary key (often a distributed ID) to guarantee a single insert or delete.
Database Optimistic Lock : Add a version field to rows; updates succeed only when the version matches.
Anti‑Repeat Token : Generate a UUID token, store it in Redis with a short TTL, and validate/delete it atomically via a Lua script.
Downstream Sequence Number : Let the downstream service generate a unique sequence number, combine it with a credential ID, and use the pair as a Redis key for deduplication.
7. Code Example – Anti‑Repeat Token Scheme
Below is a complete Spring Boot example that demonstrates the token approach.
7.1 Maven Dependencies (pom.xml)
<span style="color:#c17d37;"><?xml version="1.0" encoding="UTF-8"?></span>
<span style="color:#0c9ce5;"><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"></span>
...
<span style="color:#0c9ce5;"></project></span>7.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: 207.3 Token Utility Service (TokenUtilService.java)
<span style="color:#c17d37;">import</span> java.util.UUID;
<span style="color:#c17d37;">import</span> java.util.concurrent.TimeUnit;
<span style="color:#c17d37;">import</span> org.springframework.beans.factory.annotation.Autowired;
<span style="color:#c17d37;">import</span> org.springframework.data.redis.core.StringRedisTemplate;
<span style="color:#c17d37;">import</span> org.springframework.data.redis.core.script.DefaultRedisScript;
<span style="color:#c17d37;">import</span> org.springframework.stereotype.Service;
@Service
public class TokenUtilService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";
/** 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 the token atomically using a Lua script */
public boolean validToken(String token, String value) {
String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
String key = IDEMPOTENT_TOKEN_PREFIX + token;
Long result = redisTemplate.execute(redisScript, java.util.Collections.singletonList(key), value);
return result != null && result != 0L;
}
}7.4 Controller (TokenController.java)
<span style="color:#c17d37;">import</span> org.springframework.web.bind.annotation.*;
@RestController
public class TokenController {
@Autowired
private TokenUtilService tokenService;
@GetMapping("/token")
public String getToken() {
String userInfo = "mydlq"; // example 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 ? "正常调用" : "重复调用";
}
}7.5 Spring Boot Application (Application.java)
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}7.6 Test Class (IdempotenceTest.java)
import org.junit.*;
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;
@RunWith(SpringRunner.class)
@SpringBootTest
public class IdempotenceTest {
@Autowired
private WebApplicationContext wac;
@Test
public void interfaceIdempotenceTest() throws Exception {
MockMvc mvc = MockMvcBuilders.webAppContextSetup(wac).build();
String token = mvc.perform(MockMvcRequestBuilders.get("/token")
.accept(MediaType.TEXT_HTML)).andReturn().getResponse().getContentAsString();
for (int i = 1; i <= 5; i++) {
String result = mvc.perform(MockMvcRequestBuilders.post("/test")
.header("token", token)
.accept(MediaType.TEXT_HTML)).andReturn().getResponse().getContentAsString();
if (i == 1) {
Assert.assertEquals("正常调用", result);
} else {
Assert.assertEquals("重复调用", result);
}
}
}
}8. Summary
Idempotency is crucial for services that handle money or state changes. Choose the appropriate scheme based on business needs: primary‑key for insert/delete, optimistic lock for updates, downstream sequence numbers for distributed calls, and token‑Redis for generic duplicate‑submission protection. Proper implementation ensures reliable, repeat‑safe API behavior.
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.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.
