Concurrency Control And Transactional Integrity In Payments
Asked of: Software Engineer
Last updated

What's being tested
This topic tests whether you can preserve transactional integrity when payment requests are retried, duplicated, reordered, or processed concurrently. You should be able to combine database constraints, transaction isolation, idempotency, state-machine design, and asynchronous recovery rather than relying on a single “lock” or “exactly once” claim. Stripe cares because a race can create duplicate charges, lost refunds, incorrect balances, or an irreversible financial inconsistency. The interviewer is probing whether your design remains correct when clients time out, workers crash, and multiple requests target the same payment simultaneously.
Core knowledge
-
Idempotency keys let a client safely retry an operation after a timeout. Store
(merchant_id, idempotency_key)with the normalized request, result, and terminal status; a duplicate key must return the original result, while a reused key with different parameters should fail. -
Database uniqueness constraints are the final arbiter for duplicate creation. A unique index such as
(account_id, external_payment_id)prevents two application servers from creating the same logical payment, even if both read “not found” before either writes. -
Transactions must cover the invariant being protected, not merely individual statements. For example, reserving available balance requires reading the balance, validating it, and updating it in one transaction; otherwise concurrent withdrawals can both observe stale funds.
-
Isolation levels trade correctness against concurrency.
READ COMMITTEDpermits some write-skew patterns;REPEATABLE READprevents many rereads from changing but is database-specific;SERIALIZABLEgives the strongest model, often by aborting conflicting transactions that callers must retry. -
Row-level locks such as
SELECT ... FOR UPDATEserialize updates to a known payment or ledger account. Lock a deterministic set of rows and keep the transaction short; inconsistent lock ordering can create deadlocks, which should be detected and retried safely. -
A payment should use an explicit state machine, for example
requires_action → processing → succeededorprocessing → failed. Define legal transitions and make each transition conditional on the current version or state, preventing a late failure callback from overwriting a later success. -
Optimistic concurrency control stores a version or compares the expected state in the update:
UPDATE payments SET ... , version = version + 1 WHERE id = ? AND version = ?. If zero rows change, reload and resolve the conflict rather than silently overwriting another worker. -
External processors make true distributed transactions unavailable. Commit local intent first, call the processor with a stable processor-side idempotency key, then persist the response; a timeout means “unknown outcome,” not automatic permission to issue a second charge.
-
The outbox pattern atomically writes domain state and an outbound event into the same database transaction. A publisher later delivers the event, potentially more than once, so consumers need their own deduplication key or idempotent state transition.
-
Exactly-once processing is generally not an end-to-end property across a database, network, and payment processor. Design for at-least-once delivery plus idempotent handlers, durable correlation IDs, bounded retries, dead-letter handling, and reconciliation for permanently ambiguous cases.
-
Ledger entries should be immutable and balanced rather than repeatedly mutating a single “current balance” as the only record. For double-entry accounting, every transaction satisfies ; derived balances can be rebuilt and audited from the ledger.
-
Failure recovery must distinguish retryable from terminal errors. Deadlocks, connection resets, and transient
5xxresponses may be retried with exponential backoff and jitter; invalid payment details or a confirmed decline should transition deterministically and stop retrying.
Worked example
Representative prompt: “Design an idempotent payment creation API.”
In the first 30 seconds, I would clarify whether “create” authorizes funds, captures them immediately, or only creates an intent, and I would ask about retry behavior, consistency requirements, processor semantics, and acceptable latency. I would state that clients may retry after any timeout and that the system must never create two charges for one merchant operation. I would organize the answer around the API contract and idempotency record, the payment state machine, the database transaction and uniqueness constraints, and the processor-integration and recovery path. The request would atomically create or load an idempotency record and payment intent, while a stable processor key would make processor retries safe. A worker or request handler would use conditional state transitions such as processing → succeeded, and webhooks would be deduplicated and applied only through legal transitions. I would explicitly choose durable READ COMMITTED transactions plus unique constraints and conditional updates over holding a database lock across a slow processor call. I would explain that a processor timeout leaves the payment in processing or unknown, followed by status polling or reconciliation rather than an immediate second charge. If I had more time, I would specify retention and abuse limits for idempotency keys, observability, reconciliation jobs, and multi-region ownership rules.
A second angle
Representative prompt: “Prevent two concurrent refunds from exceeding the captured payment.”
The framing changes from duplicate request creation to enforcing a cumulative invariant: successful refunds must never exceed the captured amount. I would serialize refund allocation with a row lock or optimistic version check, calculate captured - refunded - pending_refunds, and insert the refund only if sufficient capacity remains. The external refund call would not occur inside the database transaction; instead, the transaction records an authorized refund intent and an outbox event. Retries and processor callbacks would use a refund-specific idempotency key, while a reconciliation process handles an unknown processor result.
Common pitfalls
Pitfall: Saying “we use a lock, so duplicates are impossible” is incomplete. A process crash can occur after the external charge but before the lock-protected database commit, and a lock cannot span an unreliable third-party call; use durable intent, idempotency, and reconciliation.
Pitfall: Treating a request ID as automatically idempotent misses the replay contract. Explain where the key is scoped, how parameters are compared, how the original response is stored, what happens during an in-progress duplicate, and when records expire.
Pitfall: Claiming “exactly once” without naming the failure boundary is a depth mistake. State which components provide at-least-once delivery, which operations are deduplicated, how ambiguous outcomes are resolved, and which invariant is enforced by the database.
Connections
An interviewer may pivot to double-entry ledgers, outbox and inbox processing, distributed sagas, database isolation levels, or reconciliation systems. They may also ask how multi-region writes, webhook ordering, deadlocks, and schema migrations affect the same invariants.
Further reading
-
Stripe: Idempotent requests — concrete API semantics for safely retrying payment-related requests.
-
Martin Kleppmann, Designing Data-Intensive Applications, Chapters 7–9 — strong treatment of transactions, consistency, replication, and distributed-system failure modes.
-
Pat Helland, “Life Beyond Distributed Transactions” — useful perspective on business-level transactions and recovery across service boundaries.
Related concepts
- Concurrency ControlSystem Design
- Transactional Consistency, Idempotency, And Exact ContractsSystem Design
- Adobe Transactional Integrity For Collaborative Edits
- Payment Processing And Ledger SystemsSystem Design
- Reliable Payment Processing And IdempotencySystem Design
- Adobe Transactional Integrity For Shared Documents