Mastering API Idempotency: Strategies, Code Samples, and Best Practices
Idempotency ensures that repeated API calls produce the same effect as a single call, preventing duplicate submissions, token misuse, and inconsistent data; this guide explains the concept, its importance, HTTP definitions, impact on systems, and presents four practical implementation methods—unique keys, optimistic locking, token‑based guards, and sequence numbers—with full SpringBoot and Redis code examples.
What is Idempotency?
Idempotent is a concept in mathematics and computer science where applying an operation multiple times yields the same result as applying it once.
In programming, an idempotent operation produces the same effect no matter how many times it is executed.
An idempotent function can be called repeatedly with the same parameters and will always return the same result without altering system state.
What is Interface Idempotency?
In HTTP/1.1, idempotency is defined as the property that multiple identical requests to a resource should have the same effect as a single request (excluding network time‑outs). The first request may cause side effects, but subsequent identical requests must not.
This means that repeated executions do not produce additional side effects or unpredictable results.
Why Implement Idempotency?
Typical scenarios that can cause problems without idempotency include:
Front‑end duplicate form submissions : Network glitches may prevent the user from receiving a success response, leading to repeated clicks.
Malicious repeated actions : For example, a user repeatedly voting can distort results.
Request timeout retries : HTTP clients often retry on timeout, causing duplicate submissions.
Message re‑consumption : MQ middleware errors can lead to the same message being processed multiple times.
Idempotency’s biggest advantage is that it eliminates unknown system issues caused by retries.
Impact of Introducing Idempotency
Idempotency simplifies client logic and prevents duplicate submissions, but it increases server‑side complexity and cost:
Parallel operations may need to be serialized, reducing throughput.
Additional business logic is required to enforce idempotency, complicating the code.
Therefore, consider whether idempotency is truly needed for a given API.
Idempotency in RESTful APIs
Among common HTTP methods, idempotency varies: √ Idempotent (e.g., GET, PUT, DELETE) x Not idempotent (e.g., POST) - May be idempotent depending on business logic
Solution 1: Database Unique Primary Key
Using a unique primary key leverages the database’s uniqueness constraint, suitable for insert operations to guarantee idempotency.
The primary key should be a globally unique ID (e.g., a distributed ID) rather than an auto‑increment column.
Applicable Operations
Insert
Delete
Constraints
Requires generation of a globally unique ID.
Workflow
Client sends a create request to the server.
Server generates a distributed ID, uses it as the primary key, and executes the corresponding SQL insert.
If the insert succeeds, the request is considered first‑time; if a duplicate‑key exception occurs, the record already exists and the server returns an error.
Solution 2: Database Optimistic Lock
Optimistic locking is suitable for update operations. Add a version field to the table and include it in the WHERE clause.
Applicable Operations
Update
Constraints
Requires an extra version column in the table.
Example
Assume a table with a version column. When updating, the client supplies the expected version; the SQL only succeeds if the version matches, otherwise no rows are affected, preserving idempotency.
UPDATE my_table SET price=price+50, version=version+1 WHERE id=1 AND version=5The WHERE clause ensures that a repeated execution will not affect any rows because the version has already been incremented.
Solution 3: Anti‑Replay Token
For scenarios like order submission, generate a global token (e.g., UUID) and require the client to include it in request headers.
The server stores Token → Value in Redis. Validation involves an atomic Lua script that checks the token’s value and deletes the key if it matches.
Applicable Operations
Insert
Update
Delete
Constraints
Requires globally unique token generation.
Needs Redis for storage and Lua for atomicity.
Workflow
Client requests a token; server returns a generated token string.
Client includes the token in request headers.
Server looks up the token in Redis; if found, deletes it and proceeds, otherwise returns a duplicate‑submission error.
In concurrent environments, the Redis lookup‑and‑delete must be atomic; use a distributed lock or a Lua script.
Solution 4: Downstream Unique Sequence Number
Downstream services generate a short‑lived unique sequence number (or order ID) and send it together with an authentication ID to the upstream service.
Upstream composes a Redis key from the sequence number and auth ID; if the key exists, the request is a duplicate; otherwise, store the key with a TTL and process the request.
Applicable Operations
Insert
Update
Delete
Constraints
Downstream must provide a unique sequence number.
Redis is required for key‑value verification.
Workflow
Downstream generates a distributed ID as the sequence number and calls the upstream API, attaching the sequence number and auth ID.
Upstream validates the presence of both values.
Upstream checks Redis for a key composed of the sequence number and auth ID; if it exists, reject as duplicate; otherwise, store the key with an expiration and continue processing.
Set an expiration on the Redis key; otherwise stale keys may accumulate and degrade Redis performance.
Implementation Example (Token + Redis)
The following example uses SpringBoot, Redis, and Lombok to implement the anti‑replay token solution.
1. Maven Dependencies
<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>
<!-- commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>2. 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: 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 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;
}
}4. Test Controller
@Slf4j
@RestController
public class TokenController {
@Autowired
private TokenUtilService tokenService;
/** Get a token */
@GetMapping("/token")
public String getToken() {
String userInfo = "mydlq"; // simulated 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 ? "正常调用" : "重复调用";
}
}Conclusion
Idempotency is crucial for services involving payments, orders, and other monetary transactions. Choose the appropriate strategy based on business needs: unique primary key for insert‑only scenarios, optimistic lock for updates, downstream sequence numbers for upstream‑downstream interactions, and token‑Redis combination for quick protection against duplicate submissions.
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.
