Mastering Idempotency: 4 Proven Techniques for Reliable Backend Operations

This article explains four common idempotency strategies—token, database unique index, distributed lock, and request‑body digest—detailing their core ideas, key concepts, and providing ready‑to‑copy Spring/Redis code examples to prevent duplicate requests in high‑traffic backend systems.

Top Architect
Top Architect
Top Architect
Mastering Idempotency: 4 Proven Techniques for Reliable Backend Operations

Background

Network jitter, user errors, MQ retries, or any duplicate request can cause over‑charging or duplicate shipments.

Today we explain four common idempotency implementations with copy‑ready code.

1. Token

Idea: Pre‑generate a token, then execute business logic, and discard the token after use.

Key words: pre‑generate, one‑time, Redis atomic delete.

Simplified code (with comments)

@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private StringRedisTemplate redis;

    // ① Pre‑generate token for front‑end
    @GetMapping("/token")
    public String getToken() {
        String token = UUID.randomUUID().toString();
        // 10‑minute TTL, enough for client 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("**请勿重复下单**");
        }
        // Actual order creation
        Order order = orderService.create(req);
        return Result.ok(order);
    }
}

Notes: UUID guarantees global uniqueness, Redis TTL prevents misuse. delete is atomic and concurrency‑safe. Token passed via Header keeps API semantics pure.

2. Database Unique Index

Idea: Make the business unique key a unique index; duplicate writes throw an exception.

Key words: native idempotency, exception as idempotent result.

Code example

@Entity
@Table(name = "t_payment",
       uniqueConstraints = @UniqueConstraint(columnNames = "transaction_id"))
public class Payment {
    @Id
    private Long id;
    // Payment platform transaction number
    @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("**支付成功**");
        } catch (DataIntegrityViolationException e) {
            // Exception means record already exists, avoid double charge
            Payment exist = repo.findByTxId(req.getTxId());
            return Result.ok("**已支付**", exist.getId());
        }
    }
}

Notes: Unique index guarantees 100 % deduplication at the DB level. Try‑catch converts exception to a normal response for smooth UX. No external dependencies, works with legacy systems.

3. Distributed Lock

Idea: Apply a distributed lock on “order‑id / user‑id”, proceed only after acquiring the lock.

Key words: mutex, 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 s, auto‑release after 5 s
            if (!lock.tryLock(3, 5, TimeUnit.SECONDS)) {
                return Result.fail("**处理中,请稍后**");
            }
            // Idempotent check by requestId
            if (repo.existsByRequestId(cmd.getRequestId())) {
                return Result.ok("**已扣减**");
            }
            // Real stock deduction
            repo.deductStock(cmd);
            return Result.ok("**扣减成功**");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return Result.fail("**系统繁忙**");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

Notes: Redisson’s watchdog auto‑renews the lock, avoiding deadlocks. requestId serves as an idempotent record; lock.isHeldByCurrentThread() prevents unlocking others’ locks.

4. Request Content Digest

Idea: Compute MD5/SHA256 of the request body and use it as the idempotent key.

Key words: zero‑extra 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("**重复请求**");
        }
        try {
            return pjp.proceed();
        } catch (Exception e) {
            redis.delete(key); // Release on error, 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 payload to 32 bits with negligible collision risk. setIfAbsent guarantees atomicity; on exception the key is deleted to avoid false‑positive deduplication.

Summary

方案

延迟

复杂度

外部依赖

适用场景

Token

Redis

有预生成环节:下单、支付

唯一索引

支付、注册

分布式锁

Redis/ZK

高并发抢券、秒杀

内容摘要

Redis

无预生成:转账、回调

Implementation Checklist

Identify a unique business key for each core interface.

Prefer database unique index for the lowest cost.

When concurrency is high, add a distributed lock or token; avoid over‑design.

Add monitoring/alerts so idempotency failures are detected immediately.

Feel free to discuss, ask questions, or contact the author for deeper conversation.

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.

springdistributed-lockIdempotencyTokenUnique Indexrequest digest
Top Architect
Written by

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.

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.