Design a service for sending money between two users. Walk me through APIs, database design, transaction flow, and failure handling.
How I frame it
Scope: internal wallet transfer (not ACH/wire). Clarify limits, currency, whether transfers are instant or async, and regulatory holds. Design for atomic debit+credit and idempotent API.
APIs
POST /transfers—{ to_user_id, amount, currency, note, idempotency_key }→202+transfer_idGET /transfers/{id}—PENDING | COMPLETED | FAILED+ failure reasonGET /wallets/me— balance + recent ledger entries
Database schema
wallets(user_id PK, balance, currency, version)ledger_entries(id, wallet_id, amount, type, transfer_id, created_at)— append-only audittransfers(id, from_user, to_user, amount, status, idempotency_key UNIQUE)
Debit sender + credit receiver in one transaction. Lock wallet rows in consistent order (lower user_id first) to prevent deadlock.
Transaction flow
POST /transfers (idempotency K1) 1. Lookup K1 → return existing if found 2. BEGIN TX 3. LOCK wallets (sender, receiver) ordered by user_id 4. IF sender.balance < amount → ROLLBACK insufficient 5. INSERT ledger debit + credit; UPDATE balances 6. INSERT transfer COMPLETED 7. COMMIT → publish TransferCompleted event
Failure handling
- Timeout after commit: client retries K1 → same transfer returned.
- Insufficient funds:
402, no partial state. - Downstream notification fails: transfer committed; async retry via outbox.
- Receiver account frozen: fail before debit; no half-transfer.
Walkthrough
Alice sends ₹1000 to Bob. POST /transfers with idempotency key K1. Server locks wallets (Alice id 10, Bob id 50 — order 10 first). Alice balance 5000 → 4000; Bob 200 → 1200. Response times out on mobile. Client retries K1 → server returns same transfer_id — no double debit. Bob's push notification fails; outbox retries — ledger already committed.
Weak answer
"Update both balances in two separate transactions." Crash between debit and credit loses money. "Saga with compensating transaction" overkill for internal wallets — single DB txn is simpler and ACID.
Follow-up questions
Daily transfer limits?
Aggregate check in same txn: sum(transfers today) + amount <= limit or rolling window table.
Cross-currency?
FX rate snapshot at transfer time; separate fx_transactions row; never recompute rate on retry.