Coordinating command, event, ledger, and notification idempotency in a distributed payment system.
One idempotency key cannot protect every stage of a payment.
At the command boundary, scope the key by operation and owner. Store a hash of the canonical payload. Reusing the key with the same payload returns the original result; using it with a different amount or destination produces a conflict.
Provider events need their own deduplication identity. Ledger postings need a unique business reference. Notifications need a delivery key so worker retries do not message the customer repeatedly.
command idempotency → prevents duplicate instructions
event idempotency → prevents duplicate event handling
ledger idempotency → prevents duplicate money movement
delivery idempotency→ prevents duplicate messages
These guarantees should be backed by database constraints, not only application-level checks. Concurrent requests can both pass a pre-insert existence query.
Idempotency is a set of coordinated boundaries. The goal is not merely to accept retries; it is to make every retry resolve to the same financial truth.
Canonicalize before hashing
Payload hashing only works if semantically identical requests produce the same representation. Normalize field ordering, decimal formatting, currency casing, and optional values before hashing. Never include volatile metadata such as request time unless it is part of the business command.
Store the key, scope, payload hash, processing state, and resulting resource identifier atomically. If another request finds the key while processing is still underway, it should wait briefly or return a deterministic in-progress response rather than execute again.
Let the database arbitrate races
Two requests can both observe that a key does not exist. A unique constraint on operation, owner, and key decides the winner safely. Catch the conflict and load the stored result.
The same principle applies to provider event IDs and ledger posting references.
Handle ambiguous failures
If the connection drops after a provider accepts a request, retrying with a new provider reference can duplicate the transfer. Reuse the provider idempotency key when supported and query by the original reference before creating another attempt.
Choose retention intentionally
Financial idempotency records often need longer retention than ordinary HTTP response caches. Deleting them too early can allow an old mobile retry or delayed job to recreate a completed operation.
Test payload conflicts
Tests should cover simultaneous requests, reused keys with changed amounts, worker crashes after provider acceptance, duplicate events, and repeated notification tasks.
Idempotency is successful when retries become boring: every participant can repeat its work request and the system still produces one business command, one ledger effect, and one customer outcome.
Implementation checklist
Document the idempotency identity for every externally retryable command and every background task with a financial effect. Back each identity with a unique constraint. Store completed results long enough to cover delayed clients, provider retries, and operational replay windows.
Make conflict responses explicit. A reused key with a different canonical payload should return a deterministic conflict, log the owner and operation, and never mutate the original result.
Run concurrency tests against the real database engine because in-memory tests may not reproduce locking and uniqueness behavior. The important test is not whether one request succeeds; it is whether twenty identical simultaneous requests still create one effect.
Top comments (0)