Loading…

Requirements & API: Digital Wallet

The first move in any interview: define requirements and sketch the API before drawing a single box.

Functional requirements

  • Create a wallet for a user and query their current balance.
  • Transfer money between two wallets atomically from the user's point of view (no lost or duplicated funds).
  • Record every money movement in a double-entry ledger that always balances.
  • Support deposits and withdrawals against an external payment rail (PSP or bank).
  • Replay history to rebuild balances and investigate disputes.

Non-functional requirements

  • Exactly-once money movement: a retry must never double-debit or double-credit.
  • Strong consistency per wallet shard: a wallet never shows a balance that violates its ledger.
  • Auditable: every state change is an immutable event that can be replayed.
  • Resilient to partial failure across wallets on different shards or services.

API contract

POST /v1/wallets { user_id } → { wallet_id, balance }
Creates a wallet with zero balance and opens the ledger account.
GET /v1/wallets/{id} → { balance, currency }
Reads from the CQRS balance projection, not by summing the whole ledger live.
POST /v1/transfers { from, to, amount, idempotency_key } → { transfer_id, status }
Starts a saga: debit sender, credit receiver, or compensate on failure.
POST /v1/deposits { wallet_id, amount, psp_ref, idempotency_key } → { status }
Credits the wallet after the PSP confirms funds; ledgered as external→wallet.
GET /v1/wallets/{id}/ledger?cursor= → [{ entry... }]
Paginated double-entry history for the account.

About Digital Wallet

You send $50 from your wallet to a friend's. Somewhere between 'debit me' and 'credit them', a service crashes. Did the money vanish? Did it land twice? A digital wallet's entire job is to make sure the answer is always neither. Among system design topics, this is the one where correctness is non-negotiable: every bug is somebody's money.

Here is the whole thing in plain terms. Each user has a wallet balance. A transfer is not one write; it is two: debit the sender and credit the receiver. Those two steps may live on different shards or even different services, so you cannot wrap them in a single database transaction. Instead you run a saga: try the debit, try the credit, and if either step fails, run a compensating action that undoes the earlier step. The ledger records every movement as double-entry bookkeeping (one debit, one matching credit), so you can prove the books still balance.

The pattern that makes this auditable is event sourcing plus CQRS. The write path appends immutable events (WalletDebited, WalletCredited, TransferFailed) to an event store. The read path projects those events into a balance query model optimized for 'what is my balance?'. If a projection goes wrong, you rebuild it by replaying the event log. That is reproducibility: the history is the source of truth, not the current row.

Interviewers love comparing distributed transaction options. Two-phase commit (2PC) is strong but blocks and is a poor fit for high-latency wallet services. TCC (Try-Confirm-Cancel) reserves funds first, then confirms or cancels. Saga with compensations is usually the practical choice for wallets: each local step is an ACID transaction, and failure triggers an explicit undo. This system teaches why money needs a ledger, how sagas replace distributed locks, and why an event log beats 'update the balance row' as your audit trail.