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.

Architect
Architect
Architect
Mastering Idempotency: 4 Proven Strategies for Reliable 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

RedisSpring BootDistributed LockIdempotencyTokenDatabase Index
Architect
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.