The same payment callback arrived three times and goods shipped twice—is a unique index alone enough?
The same payment callback arrived three times, and the order shipped twice—frontend button graying-out does not help here, because callbacks do not go through the page at all. Based on 2026 project delivery practice, deduplication belongs in the backend: give each business intent a unique identifier, put "has this been processed" and "write the data" into the same atomic action, and let duplicate requests receive the same result as the first one. A unique index alone can cover part of it, but if state transitions and result lookup are missing, customer service and operations still have to clean up manually.
The extra record usually is not because the user clicked twice
What needs prevention is multiple records for the same business intent, not how many times the user clicked a button. Duplicate requests often look identical in logs: same parameters, close timestamps, just processed twice.
Based on common 2026 delivery experience, duplicate sources fall into roughly several categories:
- Client or gateway timeout triggers automatic retry, while the user only sees one "Submitting" state;
- The user refreshes the page, navigates back and clicks again, or the App and mini-program place an order at the same time;
- A script bypasses the page and calls the API directly, so graying-out has no constraint on it;
- Messages and callbacks are delivered at least once, and if the consumer does not deduplicate, another record will be written;
- Real rapid clicks within a few hundred milliseconds—this is the category that frontend debouncing can block.
Taken together, the proportion covered by frontend graying-out is usually not high. Frontend deduplication is experience optimization; backend idempotency is the baseline for data correctness. They stack together, not an either-or choice.
Which layer deduplication lives in: the division of labor in the idempotency four-part setup
To judge whether a deduplication setup works, look at only one thing: when the same request arrives repeatedly, is the second processing result consistent with the first? Around this goal, delivery usually relies on four components working together; using any one alone will leave gaps.
- Idempotency key: one unique identifier per business intent; no matter how many times it is resent, it carries the same key. Granularity should align with the business—one key per order placement, not one key per session;
- Unique constraint: persist the idempotency key to a database unique index, so duplicate writes fail at the storage layer instead of checking first and inserting later in code;
- State machine: the business object transitions from a clear initial state, and duplicate requests only perform state judgment rather than creating the object again;
- Result lookup: duplicate requests return the first processing result, rather than throwing "Do not submit repeatedly."
The reason for the order is: the idempotency key determines whether the same transaction can be identified, the unique constraint determines who catches duplicates under concurrency, the state machine determines at which step duplicate requests stop, and result lookup determines what the caller sees. In 2026, a common practice for status fields is to use numbers with a dictionary table, which lowers compatibility cost when adding enum values later.
What each common approach can block
- Button graying-out and debouncing: blocks real rapid clicks; on the frontend it can be completed in about half a day to 1 person-day, but it is ineffective against refreshes, timeout retries, scripts, and callback re-delivery;
- One-time submit token: obtained when entering the form, carried back on submit, and invalidated once used by the server; it can block duplicate form submissions, but has limited coverage for multi-client concurrency and callback re-delivery, and requires extra storage and expiration cleanup;
- Idempotency key plus unique index: covers timeout retries, script API hammering, multi-client concurrency, and callback re-delivery; it involves API contracts and table structures. The typical experience range is 1–3 person-days, at the cost of periodically cleaning up idempotency records;
- Distributed lock: suitable for protecting mutually exclusive changes to the same resource, such as inventory or balance. The lock hold time should match the business duration; the typical range is hundreds of milliseconds to a few seconds. A mismatch can cause premature lock release or long request queuing.
In terms of rollout order, frontend graying-out first provides an experience fallback; during API design, decide where the idempotency key is passed; for sensitive write APIs such as amount, inventory, and coupons, add the unique index and state machine; introduce a distributed lock only when mutually exclusive access to the same resource is required. Treating a distributed lock as a general deduplication solution is usually a low return-on-investment choice.
Delivery scene: the rework when graying-out shipped first and the unique index was pushed to the next phase
In a delivery before a major promotion, budget and schedule were tight, and the client asked to add button graying-out first as a stopgap while pushing the backend unique index to the next phase. The constraint at the time was that the callback side already integrated a third party, and the re-delivery policy could not be changed. The approach was therefore to implement debouncing and graying-out only on the frontend, while the callback entry point still followed the pattern of "check the order status first, then update shipping." As a result, that night third-party callback re-delivery combined with client timeout retries, several extra coupons were issued; the next day they were manually checked and rolled back one by one. Rework took about 1–2 extra person-days and consumed that day's troubleshooting time. Graying-out is not something you cannot ship; it just cannot be the only thing you ship.
Validation: actively generate duplicate requests
- Submit concurrently with the same idempotency key; a common practice is 10–50 times. The result should create only one business record, and each response should return the same business order number;
- Manually create timeout retries and callback re-delivery, and verify that the final record count and business state match a single submission;
- Confirm that a cleanup task for idempotency records exists and will not grow indefinitely.
The counterexamples are also clear: duplicate requests only return "Do not submit repeatedly" without clarifying whether the first attempt succeeded; or two responses return different order numbers, and the caller does not know which one to trust. Replay the same request multiple times, keep only one copy of the business data, and ensure every party sees a consistent answer. Only when this criterion passes is deduplication considered closed-loop.
Applicable and non-applicable boundaries
Deduplication is not required for every API; the criterion is whether one duplicate would cause actual loss.
- Worth investing in: order placement, payment, refund, withdrawal, coupon claiming, registration, and cross-system callback processing;
- Can be simplified: low-frequency internal admin operations; a unique index is enough as a fallback, without a full state machine and result lookup;
- No need to add: read APIs, naturally idempotent write operations that set a state to a fixed value, and statistical tracking events where repeated reports only overwrite.
One more boundary needs to be stated clearly: idempotency solves repeated arrival of the same business intent; it cannot solve two genuine submissions with different intents, nor can it replace reconciliation. Reconciliation still needs to check transaction flows and business records daily or periodically. The two are complementary and cannot replace each other.
FAQ
Should the idempotency key be generated by the frontend, or is it safer for the backend to issue it?
Both work. Frontend generation is simple to implement and suits self-developed clients; backend issuance adds one more request but can cover refreshes and multi-client concurrency. Choose based on business risk.
If a unique index is already in place, is a distributed lock still needed?
Usually not. A unique index can already catch duplicate writes under concurrency; a distributed lock is better suited to protecting mutually exclusive changes to the same resource, such as inventory deduction. The two have different responsibilities.
How long should idempotency records be retained? Will keeping them forever slow down writes?
Align with the business reconciliation cycle; the typical experience range is 1–7 days, with cleanup by a scheduled task when expired. If the retention period is too long, the idempotency table becomes a large table and affects write efficiency.
Can two genuine orders placed very close together be misjudged as duplicates by the idempotency key?
Yes, if the key granularity is too coarse. The key should be distinguished by business intent—one key per order placement, not one key per user or one key per login.
Next, start by making a list of write APIs and mark the ones where one duplicate would cause loss, then confirm one by one who generates the idempotency key, which table and unique index it lands on, and what duplicate requests return. Projects with tight schedules can first ship graying-out and the unique index, and move the state machine and result lookup to later iterations; for low-frequency scenarios that can be manually covered, writing the verification process into the delivery document is also a reasonable choice.
-
Where Should API Idempotency Be Placed to Avoid Chaos in System Development?
Date: Aug 22, 2026 Read: 46
-
Payment and shipment messages for the same order arrive out of order — should the consumer maintain order itself instead of waiting for the queue?
Date: Oct 15, 2026 Read: 0
-
Ops changes campaign toggles weekly and asks devs for a release every time — should config move to a config center?
Date: Oct 14, 2026 Read: 7
-
List page 2 shows the last record from page 1 again—can frontend primary-key deduplication ship first?
Date: Oct 12, 2026 Read: 13
-
Hundreds of thousands of messages backed up in the queue, doubled the consumers and it still hasn't dropped — is the partition count the bottleneck?
Date: Oct 10, 2026 Read: 17




