Mastering Idempotency: 4 Proven Strategies for Reliable APIs
This article explains four practical idempotency solutions—token tokens, database unique indexes, distributed locks, and request content digests—detailing their concepts, core keywords, and providing ready‑to‑copy Spring Boot code examples, along with implementation tips and a comparison table to help you choose the right approach for high‑concurrency APIs.
1. Token Token — Classic and Stable
Idea: Generate a token, then execute business, token is one‑time use.
Key words: pre‑generate, one‑time, Redis atomic delete.
Simplified code (with annotations)
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private StringRedisTemplate redis;
// ① Pre‑generate token for frontend
@GetMapping("/token")
public String getToken() {
String token = UUID.randomUUID().toString();
// 10‑minute TTL, enough for frontend to place order
redis.opsForValue().set("tk:" + token, "1", Duration.ofMinutes(10));
return token;
}
// ② Order endpoint, token passed in Header
@PostMapping
public Result create(@RequestHeader("Idempotent-Token") String token,
@RequestBody OrderReq req) {
String key = "tk:" + token;
// Atomic delete: true means first use
Boolean first = redis.delete(key);
if (Boolean.FALSE.equals(first)) {
return Result.fail("**Please do not repeat the order**");
}
// Real order creation
Order order = orderService.create(req);
return Result.ok(order);
}
}Notes:
UUID guarantees global uniqueness; Redis TTL prevents misuse.
delete is atomic and naturally concurrent‑safe.
Token passed via Header keeps API semantics clean.
2. Database Unique Index — Low Cost, Strong Consistency
Idea: Make the business unique key a unique index; duplicate writes throw an exception.
Key words: natural idempotency, exception equals idempotent.
Code example
@Entity
@Table(name = "t_payment",
uniqueConstraints = @UniqueConstraint(columnNames = "transaction_id"))
public class Payment {
@Id
private Long id;
// Transaction ID from payment platform
@Column(name = "transaction_id")
private String txId;
private BigDecimal amount;
private String status;
}
@Service
public class PayService {
@Autowired
private PaymentRepo repo;
public Result pay(PayReq req) {
try {
Payment p = new Payment();
p.setTxId(req.getTxId());
p.setAmount(req.getAmount());
p.setStatus("SUCCESS");
repo.save(p); // Duplicate throws DataIntegrityViolationException
return Result.ok("**Payment successful**");
} catch (DataIntegrityViolationException e) {
// Exception means already paid, avoid duplicate charge
Payment exist = repo.findByTxId(req.getTxId());
return Result.ok("**Already paid**", exist.getId());
}
}
}Notes:
Unique index guarantees 100 % duplicate protection at the DB level.
try‑catch converts exception to normal response for smooth UX.
No external dependencies; works with legacy systems.
3. Distributed Lock — High‑Concurrency Weapon
Idea: Apply a distributed lock on “order number / user ID”; only the lock holder proceeds.
Key words: mutual exclusion, timeout, re‑entrant.
Redisson concise version
@Service
public class StockService {
@Autowired
private RedissonClient redisson;
public Result deduct(DeductCmd cmd) {
String lockKey = "lock:stock:" + cmd.getProductId();
RLock lock = redisson.getLock(lockKey);
try {
// Wait up to 3 seconds, lock auto‑releases after 5 seconds
if (!lock.tryLock(3, 5, TimeUnit.SECONDS)) {
return Result.fail("**Processing, please wait**");
}
// Idempotent check: query by request ID
if (repo.existsByRequestId(cmd.getRequestId())) {
return Result.ok("**Already deducted**");
}
// Real stock deduction
repo.deductStock(cmd);
return Result.ok("**Deduction successful**");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Result.fail("**System busy**");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}Notes:
Redisson’s watchdog auto‑renews lock, preventing deadlock.
requestId as idempotent record; lock + unique index double‑insurance.
lock.isHeldByCurrentThread() avoids unlocking others’ locks.
4. Request Content Digest — Transparent and Universal
Idea: Compute MD5/SHA256 of the request body and use it as the idempotency key.
Key words: zero‑cost interaction, client‑transparent.
Custom annotation + AOP
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
int expire() default 3600; // seconds
}
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private StringRedisTemplate redis;
@Around("@annotation(idem)")
public Object around(ProceedingJoinPoint pjp, Idempotent idem) throws Throwable {
HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// ① Compute request body digest
String body = IOUtils.toString(req.getReader());
String digest = DigestUtils.md5DigestAsHex(body.getBytes(StandardCharsets.UTF_8));
String key = "idem:digest:" + digest;
// ② First time: setIfAbsent returns true
Boolean absent = redis.opsForValue().setIfAbsent(key, "1", Duration.ofSeconds(idem.expire()));
if (Boolean.FALSE.equals(absent)) {
return Result.fail("**Duplicate request**");
}
try {
return pjp.proceed();
} catch (Exception e) {
redis.delete(key); // Release on exception, allow retry
throw e;
}
}
}
// Usage
@RestController
public class TransferApi {
@PostMapping("/transfer")
@Idempotent(expire = 7200)
public Result transfer(@RequestBody TransferCmd cmd) {
return Result.ok(transferSvc.doTransfer(cmd));
}
}Notes:
MD5 compresses any length message into a 32‑character string with negligible collision risk.
setIfAbsent guarantees atomicity; exception‑triggered rollback avoids false positives.
Annotation + AOP provides zero‑intrusion; existing endpoints need only one line.
Summary Table
Solution
Latency
Complexity
External Dependency
Applicable Scenarios
Token
Medium
Low
Redis
Pre‑generation steps: ordering, payment
Unique Index
Low
Low
None
Payment, registration
Distributed Lock
Medium
Medium
Redis/Zookeeper
High‑concurrency flash sales, seckill
Content Digest
Low
Medium
Redis
No pre‑generation: transfer, callbacks
Implementation Checklist
Check core interfaces for a unique business key.
Prefer database unique index for lowest cost.
Introduce token or distributed lock only when concurrency is high.
Add monitoring/alerting on idempotency failures.
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.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
