Ensuring API Idempotency: 4 Proven Strategies with SpringBoot & Redis
This article explains why API idempotency is crucial in real‑world systems, outlines which operations need it, discusses the trade‑offs of adding idempotency, and presents four practical implementation patterns—including unique primary keys, optimistic locking, anti‑duplicate tokens, and downstream sequence numbers—complete with SpringBoot code examples.
Idempotency Introduction
Idempotency is a frequent interview topic and a real‑world problem; duplicate message consumption caused issues in a past project, prompting this guide on ensuring API idempotency with four selectable solutions.
1. What is API Idempotency
Idempotency means that multiple identical requests produce the same result without side effects.
For example, a payment may succeed but the response fails due to a network error; a user clicks again, causing a second charge and duplicate transaction records.
2. Why Implement Idempotency
Frontend repeated form submissions due to missing success feedback.
Malicious repeated actions such as vote spamming.
Client‑side timeout retries that resend the request.
Message brokers delivering the same message multiple times.
The biggest benefit is preventing unknown issues caused by retries.
3. Operations That Need Idempotency
Among CRUD operations, creation and update are most vulnerable.
Creation
Repeated submissions can cause duplicate charges.
Deletion
Deleting once or multiple times yields the same final state (the data is gone), though response codes may differ.
Update
Setting a field to a fixed value is idempotent; incrementing a field is not.
Query
Read‑only queries are naturally idempotent.
4. Impact of Adding Idempotency
Idempotency simplifies client logic but adds server‑side complexity and cost:
Parallel operations may need to be serialized, reducing throughput.
Additional business logic increases code complexity.
Introduce idempotency only when the business scenario truly requires it.
Common Solutions for Implementing Idempotency
Solution 1: Database Unique Primary Key
Description
Leverages the unique constraint of a primary key, suitable for insert operations to ensure only one record with the same key exists.
Use a distributed ID (not an auto‑increment key) to guarantee global uniqueness in a distributed environment.
Applicable Operations
Insert
Delete
Usage Constraints
Requires generation of a globally unique ID.
Main Flow
Client sends a create request to the server.
Server generates a distributed ID, uses it as the primary key, and executes the INSERT SQL.
If the insert succeeds, the request is unique; if a duplicate‑key exception occurs, the request is a repeat.
Solution 2: Database Optimistic Lock
Description
Suitable for update operations. Add a version column to the table and include the version in the WHERE clause; the update succeeds only if the version matches.
Applicable Operations
Update
Usage Constraints
Requires an extra version column in the business table.
Example
Assume a table with columns id, name, price, version. The update statement includes the version condition:
UPDATE my_table SET price = price+50, version = version+1 WHERE id = 1 AND version = 5If the row with id=1 and version=5 no longer exists, the statement has no effect, guaranteeing idempotent updates.
Solution 3: Anti‑Duplicate Token
Description
Generate a global token (e.g., UUID) and store it in Redis with associated user data. The client includes the token in request headers; the server validates and deletes the token atomically using a Lua script.
Applicable Operations
Insert
Update
Delete
Usage Constraints
Requires generation of a globally unique token string.
Requires Redis for token storage and validation.
Main Flow
Server provides an endpoint to obtain a token.
Client calls the endpoint and receives the token.
Server stores the token as a Redis key (with expiration).
Client includes the token in request headers.
Server retrieves the token from Redis, runs a Lua script to check the value and delete the key atomically.
If validation succeeds, the business logic proceeds; otherwise, a duplicate‑submission error is returned.
In concurrent scenarios, the Redis lookup and delete must be atomic; use a distributed lock or a Lua script.
Solution 4: Downstream Unique Sequence Number
Description
The downstream service generates a short‑lived unique sequence number (e.g., order ID) and sends it with the request. The upstream service composes a Redis key from the sequence number and authentication ID to detect duplicates.
Applicable Operations
Insert
Update
Delete
Usage Constraints
Downstream must provide a unique sequence number.
Redis is required for duplicate detection.
Main Flow
Downstream generates a distributed ID as the sequence number and calls the upstream API, attaching the sequence number and authentication ID.
Upstream validates the presence of both parameters.
Upstream checks Redis for a key composed of the sequence number and authentication ID; if it exists, a duplicate error is returned; otherwise, the key/value is stored (with expiration) and the business logic proceeds.
The Redis key should have an expiration time; otherwise, stale keys may accumulate and degrade Redis performance.
Idempotent Code Example
The following demonstrates the anti‑duplicate token approach using SpringBoot and Redis.
1. Add Dependencies
<?xml version="1.0" encoding="UTF-8"?>
<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>
<name>springboot-idempotent-token</name>
<description>Idempotent Demo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- springboot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springboot data redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>2. Redis Configuration
spring:
redis:
ssl: false
host: 127.0.0.1
port: 6379
database: 0
timeout: 1000
password:
lettuce:
pool:
max-active: 100
max-wait: -1
min-idle: 0
max-idle: 203. Token Utility Service
@Slf4j
@Service
public class TokenUtilService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";
/** Create token and store in Redis */
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 */
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;
}
}4. Test Controller
@Slf4j
@RestController
public class TokenController {
@Autowired
private TokenUtilService tokenService;
/** Get token */
@GetMapping("/token")
public String getToken() {
String userInfo = "mydlq"; // mock user info
return tokenService.generateToken(userInfo);
}
/** Idempotent test endpoint */
@PostMapping("/test")
public String test(@RequestHeader(value = "token") String token) {
String userInfo = "mydlq";
boolean result = tokenService.validToken(token, userInfo);
return result ? "正常调用" : "重复调用";
}
}5. SpringBoot Application Class
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}6. Test Class
@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("Fetched token: {}", token);
for (int i = 1; i <= 5; i++) {
log.info("Call {}", i);
String result = mockMvc.perform(MockMvcRequestBuilders.post("/test")
.header("token", token).accept(MediaType.TEXT_HTML))
.andReturn().getResponse().getContentAsString();
log.info(result);
if (i == 1) {
Assert.assertEquals("正常调用", result);
} else {
Assert.assertEquals("重复调用", result);
}
}
}
}Test output shows the first call succeeds and subsequent calls are identified as duplicates.
Conclusion
Use a unique primary key for scenarios like order creation.
Apply optimistic locking for update‑heavy operations.
Downstream‑generated sequence numbers work well for upstream services.
For front‑end repeated submissions or cases without a natural unique ID, combine a token with Redis for quick idempotency enforcement.
Choose the method that matches your business logic, handle each detail carefully, and you’ll keep your system reliable.
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.
Java Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
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.
