Duplicate Payment Prevention
Duplicate payment prevention with idempotency keys, state checks, and safe retries.
Avoiding double payment means designing the system so that retries, network ambiguity, or repeated user actions do not create two successful charges for one intended purchase. This is a classic distributed systems problem wrapped in a financial workflow. The danger is not only a user pressing the button twice. It is that one side of the system may believe the payment failed while another side actually completed it.
Why duplicate charges happen
A customer may tap pay again because the app froze. A merchant server may retry after a timeout. A payment provider may receive the same callback more than once. A worker may crash after charging the card but before persisting success locally. All of these can produce duplicates if the system treats every request as new.
The core issue is uncertainty. Between "not started" and "definitely committed and recorded everywhere" there is a messy middle where one component has progressed further than another.
Idempotency is the first defence
The standard solution is an idempotency key or merchant payment reference generated before the charge attempt. Every retry for the same intended payment carries the same key. The payment service stores the first outcome for that key and returns the recorded result on repeats instead of processing a second charge.
This only works if the key represents the business operation, not the HTTP request. If each retry generates a new identifier, you have only moved the bug around.
Separate authorisation from business completion
It also helps to model the payment lifecycle explicitly. A payment can be created, authorised, captured, settled, refunded, or failed. Your local system should persist state transitions durably and reconcile them with provider events. That way a timeout after authorisation does not immediately become "charge again". It becomes "check the state of this payment reference first".
Webhooks need the same care. Providers often deliver them at least once, not exactly once. The webhook consumer must be idempotent, typically by recording the provider event ID or the resulting payment transition.
Locking and user experience
Short lived locks can prevent accidental duplicate submission from the same client session, but locks alone are not enough because they do not protect against retries across processes or later callbacks. Good UX still matters though. Disable the pay button after submission, show a pending state, and give the user a reference they can check if the response is delayed.
Reconciliation is the safety net
Even a careful design needs reconciliation. Compare internal payment records with processor reports and ledger entries so that duplicates, misses, and status mismatches are found quickly. Financial systems should assume that some edge cases will slip through the real time path.
Avoiding double payment is therefore about stable identity and state awareness. Give each intended charge one durable reference, make every component treat repeats as the same operation, and reconcile the system against external truth. In payments, certainty is designed, not assumed.