A customer has ₹500 in their wallet. They initiate two payments of ₹500 simultaneously from two different devices. How would you prevent both transactions from succeeding?
How I frame it
This is double-spend on a shared balance. Two devices = two independent HTTP requests with separate sessions — UI disable does nothing. Fix is server-side atomic debit on one wallet row.
Core solution
- Pessimistic lock:
SELECT balance FROM wallets WHERE user_id = ? FOR UPDATEinside transaction. - Conditional update:
UPDATE wallets SET balance = balance - 500 WHERE user_id = ? AND balance >= 500— second txn gets 0 rows updated → reject insufficient funds. - Append-only ledger:
wallet_transactionswith debit/credit rows; balance is cache or derived sum — audit trail for disputes. - Unique payment_id per intent: Each device creates distinct payment — both race; only one wins on balance. Retries of same intent use idempotency key.
Device A & B: POST /pay ₹500 (different payment_ids) Txn A: LOCK wallet → balance 500 → debit → 0 → COMMIT ✓ Txn B: LOCK wallet (waits) → balance 0 → UPDATE WHERE balance>=500 → 0 rows → 402 ✗
Walkthrough
User has ₹500. Phone and laptop both tap Pay at same second. Both reach server. Serializable execution on wallet row: first commit wins, second sees zero balance. User gets one success and one "insufficient wallet balance" — never -₹500.
Weak answer
"Check balance in application code then deduct." Both threads read 500 before either writes — classic lost update without lock or conditional SQL.
Follow-up questions
Redis DECR on wallet balance for speed?
Only if DB reconciles async — risk on Redis loss. Financial wallets: DB is source of truth; Redis optional read cache invalidated on write.
Transfer between two users — deadlock?
Lock wallet rows in consistent order (lower user_id first) when debiting sender and crediting receiver in one txn.
Link to P2P transfer design?
Same ledger + lock pattern extended to two wallet rows in one transaction.