skip to content

A client's call to submit a `ChargeCard` command times out on the network, so the client's retry logic resends the exact same command. How do you design the command-handling path so the retry doesn't charge the card twice, and where does that check actually have to live?

level: seniorimportance: must knowfreq 62%

answer

  1. idempotency key on the command
  2. dedup store keyed by that id
  3. check-and-write must be one atomic step
  4. natural vs enforced idempotency
  5. at-least-once delivery + dedup = effectively-once

basics

~20 s

Give each command a unique ID from the client. Before doing the real work, the handler checks whether it has already processed this exact ID - if yes, it just returns the old result instead of charging again.

solid answer

~50 s

Attach a client-generated idempotency key, or use the command's own unique ID, to every command. The handler, inside the same transaction as the side effect, checks a dedup store, a table keyed by that ID, for a prior successful execution before doing the real work; if found, it returns the previously recorded result instead of re-executing. If not found, it performs the charge and atomically records the ID plus outcome in that same transaction, so a concurrent duplicate can't slip through a check-then-act race. Some operations are naturally idempotent, like 'set balance to X', and need no extra machinery, but 'charge $50' is not naturally idempotent, so it needs this explicit dedup layer. The check must be atomic with the write; checking in one step and writing in another reopens the same race it was meant to close.

go deeper

for a junior

Knows the word idempotent roughly means safe to repeat and can give one everyday example.

for a middle

Can describe attaching a unique ID to the command and checking a dedup store before acting.

for a senior

Explains why the check-then-act must be atomic with the write, and the distinction between naturally idempotent and enforced-idempotent operations.

for a principal

Can discuss idempotency key design across service boundaries - propagating the key through async workflows and sagas, TTL and storage cost trade-offs at scale, and how this interacts with message-broker delivery guarantees such as at-least-once versus exactly-once processing semantics.

## The property and the mechanism Idempotency in command handling is the property that processing the same command more than once produces the same effect as processing it exactly once. The mechanism for enforcing it on a naturally non-idempotent operation like charging a card starts with the client attaching a unique identifier to the command before it's ever sent — either the command's own natural ID, if the client generates one, or an explicit idempotency key field. 1. **When the handler receives the command**, before performing the actual side effect, it looks up that key in a dedup store: a table or index that records which command IDs have already been processed and, ideally, what the outcome was. 2. **If the key is found**, the handler skips the real work entirely and returns the previously recorded result, so the caller gets the same answer it would have gotten the first time, without the card being charged again. 3. **If the key is not found**, the handler performs the charge and, in the same database transaction, writes a row recording that this key has now been processed, along with enough information to answer a future duplicate. ## Why the guarantee moves to the application layer This mechanism exists because network delivery is fundamentally uncertain: when a client's request times out, there is no way to distinguish 'the server never received it' from 'the server received it, processed it, and the response was lost on the way back.' A system that tried to guarantee exactly-once delivery over the network would be fighting an impossible problem in the general case — you cannot make an unreliable network reliable by trying harder at the transport layer alone. The practical, well-established solution is to **accept at-least-once delivery**, meaning duplicates will happen, and instead make duplicate processing harmless by tracking what has already been done. This shifts the guarantee from the network layer, where it can't be made, to the application layer, where it can. ## The trade-off The trade-off is extra state and extra work on every command: - **a dedup table** to maintain, - **an index** to keep fast, and - **a decision about retention** — keeping every idempotency key forever is wasteful, but purging them too aggressively reopens the double-processing window for a client that retries after a long delay. There's also a design cost in deciding what counts as 'the same command': if a client accidentally reuses an idempotency key for a genuinely different charge amount, naive implementations might silently return the wrong cached result, so many systems also store enough of the original request to detect and reject a key reused with different content, rather than assuming any repeat of the key means an identical retry. ## Failure modes 1. **The most damaging production failure mode** is implementing the dedup check and the actual write as two separate, non-atomic steps: first a SELECT to see if the key exists, then, if not found, a separate step to perform the charge and insert the key. Under concurrency — two duplicate requests arriving close together, which is exactly the scenario retries create — both can pass the SELECT before either has inserted its key, and both proceed to charge the card. The fix is to make the check and the write atomic with each other, typically by relying on a unique constraint on the idempotency key column and letting the second insert fail with a constraint violation, which the handler catches and treats as 'already processed,' or by doing the check-and-insert inside one transaction with appropriate isolation. 2. **A second failure mode is confusing naturally idempotent operations with ones that need enforcement**: an operation like `SetShippingAddress`, which just overwrites a field, produces the same end state no matter how many times it's applied and needs no dedup key at all, while an operation like `AddItem` or `ChargeCard`, whose effect compounds with each application, absolutely does. | Naturally idempotent | Needs enforcement | |---|---| | `SetShippingAddress` just overwrites a field | `AddItem` or `ChargeCard` compounds with each application | | produces the same end state | needs an explicit idempotency key | ## A real-world pattern A concrete real-world pattern: payment APIs such as Stripe's charge endpoint require callers to supply an `Idempotency-Key` header on write operations; the server stores that key alongside the result of the first successful request for a bounded retention window, and any subsequent request with the same key returns the original result verbatim rather than creating a second charge — which is precisely the client-times-out-and-retries scenario this question describes, solved at the API boundary rather than left to the client to avoid retrying altogether.

  • Why not just make the network call exactly-once instead of solving this at the command layer?
    Exactly-once delivery isn't achievable over an unreliable network in the general case - you can't know whether a timeout means the server never got the request or got it and just failed to reply. The standard solution is at-least-once delivery paired with idempotent processing, which sidesteps the impossible delivery guarantee by making duplicates harmless at the receiving end.
  • Where do you store the idempotency key and how long do you keep it?
    Typically a dedicated table, or a unique constraint on the aggregate's write table, storing the key alongside the outcome or enough to reconstruct the response, so a duplicate can be answered without re-running side effects. Retention is usually time-bounded, long enough to cover realistic client retry windows, minutes to a few days, after which old keys are purged so the table doesn't grow forever.
  • What's the difference between a command that's naturally idempotent and one that needs an explicit idempotency key?
    A naturally idempotent command like SetShippingAddress produces the same end state no matter how many times it's applied, so no extra tracking is needed. A command like ChargeCard or AddItem is not naturally idempotent, since applying it twice doubles the effect, so it needs an explicit key-based dedup mechanism to be made safe against retries.

It's like a wedding RSVP card with a unique guest ID printed on it. If the same RSVP card gets mailed in twice, maybe the post office duplicated it, the venue checks its guest list by that ID before adding a seat. Seeing the ID already checked off, they don't set a second place at the table; they just confirm the existing seat is still there.

saying these in an interview costs you the question

  • Thinks idempotency is just about the HTTP method being PUT vs POST, with no mention of a dedup mechanism for actual side effects
  • Checks for a duplicate command in a separate read before the transaction that performs the write, leaving a race window
  • Assumes retries are rare enough to ignore in a distributed system
  • Can't explain the difference between naturally idempotent operations and ones that need an explicit key
  • Proposes exactly-once delivery as the fix instead of at-least-once plus idempotent handling

context