Understanding Idempotency and Its Implementation in RESTful APIs
This article explains the concept of idempotency, why it is essential for HTTP interfaces, the impact on system design, and presents several practical implementation strategies—including unique primary keys, optimistic locking, anti‑repeat tokens, and downstream sequence numbers—accompanied by a complete Spring Boot example with Redis integration and test code.
Idempotency is a mathematical and computing concept where applying an operation multiple times yields the same result as applying it once; in programming, an idempotent operation produces identical effects regardless of how many times it is executed.
HTTP/1.1 defines idempotent methods, meaning that repeated requests to a resource should have the same effect as a single request (excluding network errors). This property prevents unintended side effects such as duplicate form submissions, malicious vote spamming, client‑side retries, or message re‑consumption.
Implementing idempotency simplifies client logic but adds complexity on the server side, potentially reducing parallelism and requiring extra business logic.
RESTful HTTP methods and their idempotency status:
GET, HEAD, PUT, DELETE, OPTIONS, TRACE – idempotent (✓)
POST – not idempotent (✗)
PATCH – may be idempotent depending on business logic (–)
Four common server‑side solutions are described:
1. Database Unique Primary Key
Use a globally unique primary key (often a distributed ID) to ensure that insert or delete operations cannot be duplicated. The database’s unique constraint guarantees idempotency.
2. Optimistic Lock
Add a version column to a table; each update includes the expected version value. The SQL
UPDATE my_table SET price=price+50, version=version+1 WHERE id=1 AND version=5succeeds only once because the version changes after the first execution.
3. Anti‑Repeat Token
Clients obtain a token (e.g., UUID) from the server, store it in Redis with a short TTL, and include it in request headers. A Lua script atomically checks the token’s existence and deletes it, ensuring a single successful execution.
Lua script used:
if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end4. Downstream Unique Sequence Number
The downstream service generates a short‑lived unique sequence number (or order number) and sends it with the request. The upstream service stores the combination of sequence number and authentication ID in Redis; duplicate detection is performed by checking the key.
Below is a complete Spring Boot demonstration implementing the anti‑repeat token approach.
Project Setup (Maven)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<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>
<build>...</build>
</project>Redis Configuration (application.yml)
spring:
redis:
ssl: false
host: 127.0.0.1
port: 6379
database: 0
timeout: 1000
lettuce:
pool:
max-active: 100
max-wait: -1
min-idle: 0
max-idle: 20Token Utility Service
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
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.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
@Slf4j
@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 token atomically using Lua */
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";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
String key = IDEMPOTENT_TOKEN_PREFIX + token;
Long result = redisTemplate.execute(redisScript, 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;
}
}Controller for Token Retrieval and Idempotent Test
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(value = "token") String token) {
String userInfo = "mydlq";
boolean result = tokenService.validToken(token, userInfo);
return result ? "正常调用" : "重复调用";
}
}Spring Boot Application Entry
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);
}
}JUnit Test Demonstrating Idempotency
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import lombok.extern.slf4j.Slf4j;
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();
log.info("Token: {}", token);
for (int i = 1; i <= 5; i++) {
String result = mockMvc.perform(MockMvcRequestBuilders.post("/test")
.header("token", token).accept(MediaType.TEXT_HTML))
.andReturn().getResponse().getContentAsString();
log.info("Attempt {}: {}", i, result);
if (i == 1) {
Assert.assertEquals("正常调用", result);
} else {
Assert.assertEquals("重复调用", result);
}
}
}
}In summary, idempotency is crucial for services involving financial transactions or any operation that must not be executed multiple times unintentionally. Choose the appropriate strategy based on business requirements: unique primary keys for creation, optimistic locking for updates, downstream sequence numbers for inter‑service calls, or anti‑repeat tokens for generic scenarios.
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.
