Ensuring Idempotency in Order Services: Preventing Duplicate Orders and Solving the ABA Problem
This article explains how to achieve idempotent order creation and updates by using unique order IDs, database primary key constraints, Redis flags, and version columns to prevent duplicate submissions and resolve the ABA problem, ensuring consistent data across distributed services.
1. Problem Background
The simplest case is a DB transaction: when creating an order, inserts into the order table and order‑item table must be executed within the same transaction.
If the Order service calls the Pay service and a network timeout occurs, the Order service may retry, causing the Pay service to receive the same payment request twice. Because load‑balancing may route the requests to different nodes, the distributed system must guarantee idempotency.
2. How to Avoid Duplicate Orders
Front‑end forms can block repeated submissions, but network errors can trigger retries, and many RPC frameworks or gateways have automatic retry mechanisms, so duplicate requests cannot be fully prevented on the client side. The core issue is ensuring the service interface is idempotent.
2.1 How to Determine if a Request Is Duplicate
Check the order table before inserting to see if a duplicate exists, but defining “duplicate order” in SQL is difficult.
Is an order with the same user, product, and price a duplicate? What if the user intentionally places two identical orders?
To achieve idempotency, the following must be done:
2.1.1 Each request must have a unique identifier
For example, a payment request must include an order ID, and an order ID can only be successfully paid once.
2.1.2 After processing a request, record a flag indicating the request has been handled
In MySQL, add a status field or a payment‑flow record for the order before payment.
2.1.3 When receiving a request, check whether it has been processed before
If an order has already been paid, a payment flow record exists. A duplicate request will attempt to insert the same order_id and trigger a unique‑key violation, preventing double charging.
When inserting a record, usually the primary key is auto‑generated. If the INSERT statement provides a primary key that already exists, the statement fails. Therefore, you can rely on the DB’s primary‑key uniqueness constraint to achieve idempotent order creation by supplying the primary key during insertion.
Provide an "orderId generation" API that returns a globally unique order number. The front‑end calls this API before showing the order creation page, obtains the order number, and includes it in the create‑order request.
The order number becomes the primary key of the order table, so duplicate requests carry the same order number. When the order service inserts the order, the duplicate INSERT statements all use the same primary key, and the DB’s unique constraint ensures only one succeeds.
In practice, combine this with Redis: use the orderId as a unique key. Only after successfully inserting the payment flow record should the system mark the order as paid in Redis.
When paying an order, insert a payment flow record with a unique order_id key. Then set a Redis key: set order_id payed If a later request finds the Redis value "payed", it knows the order has already been processed and skips duplicate payment.
If a duplicate order causes insertion of t_order to fail, the Order service should not return the error to the front‑end; otherwise the user may see a failure message while the order was actually created.
Correct approach: the order service should return success even if the duplicate insertion fails, because the order already exists.
3. Solving the ABA Problem
3.1 What Is the ABA Problem?
After an order is paid, the seller fills in a tracking number. Suppose the seller first enters "666", then realizes it’s wrong and changes it to "888". Two update requests are sent: one with 666 and one with 888. If the system crashes after processing 666 but before the response reaches the caller, the caller may retry, sending 666 again. The final state could incorrectly be 666, even though the intended final value is 888.
3.2 Solution
Add a version column to the order main table. Each time the order is fetched, the version number is returned to the client. When updating, the client sends the version back, and the update statement checks that the version matches.
The update must compare the version, modify the data, and increment the version, all within a single transaction.
UPDATE orders SET tracking_number = 666, version = version + 1 WHERE version = 8;If the version in the WHERE clause does not match, the update is rejected. If it matches, the update succeeds and the version is incremented.
If the request carries an old version (e.g., 8) after the tracking number was already updated to 888 (new version 9), the update fails and the user sees a failure message.
If the request carries the new version, the update succeeds, and any later retry of the old request fails because the version has changed.
This guarantees that the DB state and the user’s view remain consistent, achieving idempotent updates and avoiding ABA.
4. Summary
For order creation, pre‑generate a unique order number and rely on the DB’s unique constraint to ensure idempotent order insertion.
For order updates, use a version‑column mechanism: verify the version before updating, and increment the version atomically to solve the ABA problem and guarantee idempotent updates.
These two idempotency techniques can be applied to any service that persists data in a database with a primary‑key table.
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.
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.