← Home
💬Real-life Scenario-based Interview QuestionsInterview QuestionsNEW
Important
Experience levelSenior · 5+ YOESenior / staff depth expected

Design a service for sending money between two users. Walk me through APIs, database design, transaction flow, and failure handling.

PaymentsLLDDatabaseSystem Design

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_id
  • GET /transfers/{id} — PENDING | COMPLETED | FAILED + failure reason
  • GET /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 audit
  • transfers(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.