How an idempotency key stops a duplicate transfer from ever happening twice
1 Sept 2026 · 3 min read
You tap send. The spinner hangs. You tap it again. Did you just send the money twice?
On PrimeEx, no and not because of a client-side guard that disables the button after one click (those help, but they don't survive a retried request from a flaky connection, a mobile app resuming from the background, or a client bug). The real guarantee lives on the server, in two layers.
Layer one: a fast-path cache, keyed by what you actually asked for
Every write request that matters carries an idempotencyKey your client generates once per user action. The first layer is an interceptor that checks whether this exact combination, the user, the route, the idempotency key, and a hash of the request body, has already produced a response. If it has, it replays that response immediately, without re-running anything.
That last part, hashing the body, matters more than it sounds like it should. Without it, reusing the same idempotency key with a different payload would silently return the original, unrelated result, a real bug we found and fixed during our pre-frontend review, not a hypothetical one. The fix was small: include the payload in the cache key, not just the idempotency key by itself.
Layer two: the source of truth doesn't trust the cache
A cache is still just a cache, it can miss, expire, or (on a bad day) be wrong. So the actual guarantee doesn't live there. It lives as a unique constraint at the database level, on (user_id, idempotency_key) in the transactions table. Two rows with the same user and the same key simply cannot both exist. The database enforces it, not application logic that could have a bug in it.
On top of that constraint, when a request reuses a key we've seen before, we compute a fingerprint of the fields that define the request's financial intent, amount, currency, destination, and so on, deliberately excluding anything server-generated like IDs or timestamps and compare it to what was stored the first time. A mismatch is rejected outright, with its own error code, rather than quietly replaying whichever result happened to be sitting in the cache:
This idempotency key was already used for a request with different details. Use a new idempotency key for a different request.
Why two layers, not one
The cache makes retries fast and cheap, a genuinely duplicate request never even reaches the transfer logic a second time. The database constraint makes the guarantee real, it holds even if the cache is down, cold, or simply hasn't seen this request before. Neither layer alone is enough on its own; together, they're the same standard every money-movement flow on the platform uses, from a bank transfer to a crypto withdrawal to a currency conversion.
The result, from where you're sitting: tap send twice by accident, and exactly one transfer happens. Every time.
Related posts
Why we built the ledger first
Before a single customer-facing screen existed, PrimeEx had a double-entry ledger. Here is why that ordering mattered.
18 Aug 2026 · 3 min read
Why Nigeria-first, not global-first
Building for one market real identity system and one set of real rails before expanding anywhere else.
10 Sept 2026 · 2 min read