Idempotency and retries interview questions

7 database interview questions on idempotency and retries, each with the correct answer and a full explanation.

A payment endpoint dedups retries like this:

SELECT 1 FROM payments WHERE idem_key = $1;
-- if no row: charge card, then
INSERT INTO payments (idem_key, ...) ...;

A client times out and retries, so two requests with the same key arrive together. What prevents a double charge?

Show answer

Answer

A UNIQUE constraint on the key, and inserting the key row before calling the card network

Inserting the key first makes the database pick one winner: the second insert hits the unique violation and does not charge. The row then records the in-flight state and later the stored response.

A transaction does not stop both SELECTs from seeing no row. In-memory sets miss retries routed to another server or after a restart. A plain index makes the race faster, not impossible.

Source: Designing robust and predictable APIs with idempotency, Brandur Leach, PostgreSQL Documentation: INSERT (ON CONFLICT)

Your idempotency layer has a row for key K in state in_progress because the first request is still talking to the bank. A retry with K arrives. Following Stripe's approach, what do you return?

Show answer

Answer

An error such as 409 Conflict telling the client the original request is still in progress

Stripe returns a conflict for concurrent use of the same key. The client retries later and then gets the stored result.

Processing again is the double charge you are preventing. A 200 with no result lies about the outcome. Deleting the in-flight row lets the duplicate run alongside the original.

Source: Stripe API Reference: Idempotent requests, Designing robust and predictable APIs with idempotency, Brandur Leach

Your service charges a card through a provider that supports idempotency keys, then crashes before saving the result. The client retries. How do you avoid charging twice?

Show answer

Answer

Send the provider the same idempotency key on the retry so it returns the original charge

The external call cannot be undone by your rollback, so dedup has to reach the provider. Deriving the provider key from your own key makes the retry safe, and your row moves from in_progress to done once the result is saved.

A local rollback does not reverse a charge that already happened. Charging again is the bug. Payment providers do not take part in 2PC with your database.

Source: Designing robust and predictable APIs with idempotency, Brandur Leach, Every question I was asked in Stripe's system design interview

A client reuses idempotency key K, but the second request has amount 50 where the first had 30. Following Stripe's behaviour, what should the server do?

Show answer

Answer

Reject it with an error, because the parameters differ from the original request

Stripe compares parameters with the original and errors if they differ, which catches client bugs that reuse keys.

Treating it as new breaks the key's meaning. Silently replaying the old response hides the bug and the client thinks 50 was charged. Mutating the original changes a completed payment.

Source: Stripe API Reference: Idempotent requests

After saving an order you must publish OrderCreated to Kafka. A crash between the two steps must never lose the event or publish one for an order that was not saved. What do you do?

Show answer

Answer

Insert the event into an outbox table in the same transaction and relay it to Kafka afterwards

One local transaction writes both the order and the outbox row, so they exist together or not at all. A relay or CDC process publishes and may resend, so consumers must be idempotent.

Publishing first can announce an order that then fails to commit. Publishing after commit loses the event if the process dies in between. Kafka does not join an XA transaction with Postgres.

Source: Pattern: Transactional outbox, Chris Richardson, microservices.io, Debezium Documentation: PostgreSQL connector

A multi-tenant API lets each merchant send its own idempotency keys, which are free-form strings. Which constraint is right?

Show answer

Answer

UNIQUE (merchant_id, idem_key)

Keys are only meaningful per client. Scoping by merchant stops one merchant's key from blocking or replaying another merchant's request, while still deduping within a merchant.

A global unique key lets two merchants who both send order-1 collide, and could leak a response across tenants. Adding created_at makes every retry unique, so nothing is deduped. A non-unique index enforces nothing.

Source: Stripe API Reference: Idempotent requests

Design the idempotency layer for a POST /payments endpoint that calls an external card processor.

Show answer

Answer

Table keyed by UNIQUE (account_id, idem_key) storing request hash, state (started, external_called, done), response code and body. On a request, insert the row first: a unique violation means look up the existing row, return the stored response if done, 409 if in progress, and an error if the request hash differs.

Call the processor with a key derived from yours so its side dedups. Save each step as you go so a retry resumes from the last saved step. Expire keys after about 24 hours. Publish events through an outbox in the same transaction.

Source: Designing robust and predictable APIs with idempotency, Brandur Leach, Stripe API Reference: Idempotent requests

These questions come from the Database Interview Practice pack in Bertopia.

More database topics

Practise these on your commute