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.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Mastering Idempotency: Design Patterns and Code Examples for Reliable APIs

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
end

6. 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;
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.

Distributed Systemsoptimistic lockIdempotencyTokenpessimistic-lock
Su San Talks Tech
Written by

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.

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.