Mastering Idempotency: Design Patterns and Code Examples for Reliable APIs
This article explains the concept of idempotency, outlines scenarios where it is essential, analyzes common causes of idempotency issues, and presents multiple practical solutions—including unique constraints, optimistic and pessimistic locks, distributed locks, token mechanisms, state machines, deduplication tables, and global request IDs—accompanied by concrete code examples.
What Is Idempotency?
Idempotency means that executing an operation once or many times yields the same result without side effects. In API design, the same request should produce consistent outcomes regardless of repeated clicks, such as a bus card payment that should not be charged twice.
Note: Idempotency problems can arise in databases, but they are not limited to them.
Scenarios Requiring Idempotent Design
Typical high‑risk scenarios include:
Online Payment : Prevent duplicate charges when a user initiates payment.
Bank Transaction : Ensure a single transaction is not executed multiple times due to retries.
Ticketing System : Avoid double‑booking of seats on an online ticket platform.
Communication Service : Check whether a message or call request has already been billed.
Task Scheduling : Prevent repeated execution of the same job after a restart or retry.
User Registration : Stop multiple creations of the same user record from duplicate form submissions.
How Idempotency Problems Occur
Common causes are:
1. Network request retry: Clients resend the same request due to network glitches or timeouts.
2. UI duplicate submission: Users may unintentionally click a button multiple times.
3. Message‑queue retry mechanism: Messages can be consumed repeatedly in systems like Kafka or RabbitMQ.
4. Database concurrent operations: Multiple transactions modify the same record without proper locking or isolation.
5. External API retry: Downstream services may be called repeatedly by the caller’s retry logic.
6. Other: Various edge cases.
Case Study: Order Table Design
First, design an order table with sample data.
1. Table Structure
2. Field Description
order_id : Unique identifier, often a UUID or distributed ID (e.g., Snowflake).
user_id : Links the order to the user.
product_id : Links the order to the purchased product.
quantity : Number of items purchased.
order_status : Current status, used to control workflow and ensure idempotency (e.g., only allow payment when status is "Pending").
create_time : Timestamp of order creation.
pay_time : Timestamp of payment.
version : Optimistic‑lock version number, incremented on each update.
3. Business Rules
Order Payment : Before payment, check order_status is "Pending"; if so, update status to "Paid"; otherwise reject.
Order Cancellation : Allow cancellation only when the order is in a specific status.
Insert Order : Use order_id as a unique constraint to prevent duplicate inserts.
Optimistic Lock : Update only if the stored version matches the version read; otherwise abort.
Idempotency Solutions
Common techniques in distributed systems include:
1. Unique Constraints
Leverage database unique indexes or primary keys to avoid duplicate rows.
mysql> INSERT INTO `mydb`.`orders` (`order_id`,`user_id`,`product_id`,`quantity`,`order_status`,`create_time`,`pay_time`,`version`) VALUES ('ORD-20231023-0001','USR-A123456','PRD-X123',2,0,'2023-10-23 10:15:30',NULL,1);
ERROR 1062 (23000): Duplicate entry 'ORD-20231023-0001' for key 'orders.PRIMARY'2. Optimistic Lock
Record a version number or timestamp and update only when it matches.
UPDATE orders
SET quantity = 1,
order_status = 1,
pay_time = '2024-04-30 10:20:00',
version = version + 1
WHERE order_id = 'ORD-20231023-0001' AND version = 1;3. Pessimistic Lock
Lock rows during a transaction using SELECT ... FOR UPDATE.
-- Lock the record
SELECT * FROM orders WHERE order_id = 'ORD-20231023-0001' FOR UPDATE;
-- Execute business logic
UPDATE orders SET quantity = 1, order_status = 1, pay_time = '2023-10-23 10:20:00' WHERE order_id = 'ORD-20231025-0003';Note: Pessimistic locks can cause performance issues and deadlocks under high concurrency.
4. Distributed Lock
Use a distributed lock (e.g., Redis) to ensure only one instance processes a specific request.
public class MyService {
private final RedisDistributedLock lock;
public MyService(Jedis jedis, String lockKey, int lockTimeout) {
this.lock = new RedisDistributedLock(jedis, lockKey, lockTimeout);
}
public void executeInLock() {
if (lock.tryLock()) {
try {
// business logic
} finally {
lock.unlock();
}
} else {
// handle lock acquisition failure
}
}
}It is recommended to use a Lua script to delete the lock atomically.
public void unlock() {
String unlockScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
jedis.eval(unlockScript, 1, lockKey, "1");
}5. Token Mechanism
Generate a unique token for each request, store it in Redis, and delete it after processing to prevent repeats.
void do(String token) {
if (Redis.exists(token)) {
// delete token to ensure no repeat processing
Redis.del(token);
// execute business logic
doSometing();
} else {
log.info(token);
}
} if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end6. State Machine
Use a state machine to enforce that operations occur only in allowed states.
public enum OrderStatus { PENDING, PAID, CANCELLED }
public class Order {
private OrderStatus status;
public Order() { this.status = OrderStatus.PENDING; }
public synchronized void pay() {
if (this.status == OrderStatus.PENDING) {
// payment logic
this.status = OrderStatus.PAID;
} else {
throw new IllegalStateException("Order can only be paid when status is PENDING");
}
}
}7. Deduplication Table
Maintain a table of processed request identifiers and clean it periodically.
boolean isDuplicate = checkDuplicateInDatabase(requestId);
if (isDuplicate) {
return previousResult;
} else {
doSomthing();
saveRecord(requestId);
return newResult;
}8. Global Unique Request ID
Generate a globally unique ID for each request and store it in Redis set for deduplication.
proxy_set_header X-Request-Id $request_id;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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
