Idempotent payments / Design note (conceptual)
What happens when the happy path ends?
A provider can accept a request while its response is lost. A reliable design keeps the payment pending, verifies the provider outcome, and reconciles before retrying the money movement.
- 01Client + key
- 02Payment record
- 03Provider adapter
- 04Reconciliation
01 Problem: the response can disappear
A network timeout cannot tell us whether a provider accepted a payment. Retrying immediately can move money twice. The system needs to distinguish a rejected request from an unknown outcome.
02 Make the request durable
Scope an idempotency key to the caller and operation, enforce uniqueness in the database, and store a fingerprint of the request. Reusing a key with different input should fail. Persist a pending payment before calling a provider.
03 Handle callbacks as untrusted, repeatable events
Authenticate the provider callback, validate amount and currency, and deduplicate its event identifier. Apply allowed state transitions in a transaction using a lock or conditional update. A late pending event must not overwrite a confirmed success.
04 Reconcile before retrying
For an unknown outcome, query the provider or reconcile against settlement records. Use bounded retries with backoff for safe status checks. If the provider cannot confirm an outcome, route it to manual review instead of guessing.
05 Keep ledger updates atomic
Where a ledger is required, balanced debit and credit entries should commit together with the local payment transition. An outbox can publish follow-up events after that commit; consumers still deduplicate.
06 Trade-off: more states, more operational work
Durability buys correctness with extra states, reconciliation jobs, and a manual-review path. That cost is only worth paying where a duplicated or lost payment is unacceptable.