SDSystem design
Start learning free

LESSON 66 OF 140 · DESIGN: BOOKING & MONEY

SYSTEM DESIGN · INTERVIEW WALKTHROUGH

Design a Digital Wallet: System Design Interview, Step by Step

What the interviewer is asking

"Design a digital wallet" means an app where users keep a balance and send each other money in a second. Unlike a payment system, a transfer never leaves your system: Ana's $30 moves from her account to Ben's inside your own databases, so your records are the only proof of who owns what.

The interviewer listens for correctness under load, even when the two accounts live on different database servers. The walkthrough follows the 8 steps of the interview framework: scope, data, API, busy path, numbers, services, scale and deep dives.

1. Scope: what version 1 must do

  • Keep a balance for each user.
  • Top up the balance from a card, through the payment system.
  • Send money to another user, instantly.
  • See your balance and history.

Three rules hold everywhere, so say them out loud: money is never lost or made up, no balance goes below $0, and each transfer happens exactly once, even when the app retries. Park other currencies and exchange, cash-out to bank accounts, and rewards.

2. Data: accounts, a ledger, transfers

record     fields
account    id, balance
entry      transfer, account, amount (+ in, − out)
transfer   id, from, to, amount, status, idempotency_key

The ledger is the double-entry ledger from the Payment System lesson: each movement is two entries that sum to zero, and rows are only ever added. Ana's $30 to Ben, transfer t1, is:

entry      amount
t1 · Ana   −$30
t1 · Ben   +$30

The account's balance is a copy of the sum of its entries, kept in sync by writing both in the same transaction, so reading it is one lookup instead of adding up years of history. The transfer holds the request, its status (pending, done or failed) and the idempotency key. Money from cards comes in through a top-ups account: a $50 top-up is Ana +$50 and top-ups −$50, so all entries still sum to zero.

3. The API: three calls

POST /transfers
Authorization: Bearer <Ana's token>
{"to": "ben", "amount": 3000, "idempotency_key": "k1"}
→ 201 Created  {"id": "t1"}

GET /accounts/me
→ 200 OK  {"balance": 7000}

GET /accounts/me/history?cursor=…
→ 200 OK  20 entries and a next_cursor

Amounts are whole cents, as in the Payment System lesson. The idempotency key (the Idempotency Keys lesson) makes a retry find t1 instead of sending $30 again. There's no from field: the sender is whoever the sign-in token says is calling (the Authentication and API Keys lesson), or anyone could spend anyone's money. History is paged with a cursor (the Pagination lesson).

4. The busy path: one transfer

  1. The app sends POST /transfers: Ben, $30, key k1.
  2. The API checks that no transfer has key k1 yet.
  3. In one transaction, it checks that Ana has at least $30.
  4. It takes $30 from Ana, adds $30 to Ben, and writes both entries and t1.
  5. It commits, all or nothing, and answers 201 with t1.

The key comes first, so a retry gets t1's first answer back instead of moving the money again. Everything else is one transaction (the Transactions and Isolation lesson): the money moves all at once or not at all. t1 is saved with its key where keys must be unique, so two copies of the same request can't both commit.

5. The numbers

From the interviewer: 100 million users, and about 1 million transfers in the busiest minute of the year.

quantity             working                     result
transfers a second   1 million ÷ 60 s            ≈ 17,000 (16,667 exactly)
writes a second      17,000 × 5 rows             ≈ 85,000
shards               85,000 ÷ 5,000 a server     17 at least; say 20
writes per shard     85,000 ÷ 20                 ≈ 4,250 a second
accounts             100 million × 500 bytes     ≈ 50 GB

Each transfer writes 5 rows: t1, two balances and two entries. Say one database server handles about 5,000 writes a second: one server would be asked 17 times what it can take. The data is small (about 50 GB of accounts); the writes don't fit. A common slip is dividing by 3,600, an hour's seconds: about 280 a second.

6. Services

  • Wallet API: balances, transfers and history; it checks that the sender has the money.
  • Payment system: charges cards for top-ups, retries with the same key, and reconciles with its provider each night. Once a charge succeeds, it calls the wallet with its payment id as the idempotency key.
  • Transfer queue and workers: finish the transfers whose two accounts live on different shards (below).

7. Scale: shard the accounts

Read replicas don't help: every write still goes to one leader. Nor does a cache: transfers must write, and a cached balance can be out of date. Shard the accounts by a hash of the user id (the Sharding lesson): each shard holds some users' accounts and their entries, and takes only their writes.

The catch: a hash spreads users at random, so Ben is on Ana's shard only 1 time in 20. 19 transfers in 20 cross shards, and no database transaction spans two shards.

8. Deep dive: one shard

When both accounts share a shard, the transfer is one transaction that first locks the two account rows (SELECT … FOR UPDATE), lower account id first. With one order for everyone, two transfers can never wait on each other in a circle: no deadlock (the Concurrency and Locks lesson).

Ana (#1) has $100, and two of her transfers arrive at once: t2, $70 to Cy (#3), and t3, $50 to Dee (#4).

step   what happens                                 Ana    Cy    Dee
1      t2 and t3 arrive together                    $100   $20   $10
2      t2 locks Ana, then Cy; reads $100 ≥ $70      $100   $20   $10
3      t3 asks for Ana's lock and waits             $100   $20   $10
4      t2 moves $70, writes its entries, commits    $30    $90   $10
5      t3 gets the lock, reads $30 < $50: declined $30    $90   $10

The lock makes t3 read after t2's commit. Without it, both would check $100 before either wrote, both would pass, and Ana would end at −$20. Serializable isolation would also stop it, by aborting one transaction for a retry; locking just the two rows is the cheaper, common choice.

9. Deep dive: two shards

Ana is on shard 1 and Ben on shard 2. If the API commits the debit on shard 1 and crashes before the credit on shard 2, the $30 is gone: the total drops from $120 to $90, and nothing says the money was on its way.

Two-phase commit

A coordinator asks both shards to prepare (lock the row, write the change, promise to commit), then tells both to commit once both said yes. Both rows stay locked through both rounds of messages, and a prepared shard may not decide alone: if the coordinator dies after prepare, the rows stay locked until it comes back. With 19 transfers in 20 crossing shards, that's most of the traffic. Some databases replicate the coordinator with consensus (the Consensus and Leader Election lesson), at the price of more round trips.

A saga: debit first, credit after

  1. On shard 1, one transaction: Ana −$30, her entry, and t1 saved as pending.
  2. The API puts t1 on the transfer queue.
  3. A worker takes t1 and, in one transaction on shard 2, adds $30 to Ben, writes his entry and records t1's credit.
  4. The worker marks t1 done on shard 1.

Until Ben is credited, the $30 is in flight: out of Ana's balance, recorded in t1. Balances plus money in flight never change. The queue delivers at least once, and the credit is keyed by t1, so a second delivery adds nothing. Never queue before the commit, or a worker could credit Ben for a debit that then failed; if the API dies before queuing, a sweep re-queues pending transfers older than a minute.

Here a worker crashes after the credit and before marking t1 done:

moment                          Ana    t1        Ben   in flight   total
before t1                       $100   –         $20   $0          $120
shard 1 commits the debit       $70    pending   $20   $30         $120
a worker credits Ben            $70    pending   $50   $0          $120
the worker crashes              $70    pending   $50   $0          $120
another worker runs t1 again    $70    done      $50   $0          $120

Without the check by transfer id, the rerun would give Ben $80 and the books would show $150: $30 made up. If the credit can never land (Ana sends Eli $40 as t4, and Eli's account is closed), the worker runs the saga's compensating step: a new movement of $40 back to Ana, keyed by t4, and t4 marked failed. Ana goes from $30 back to $70, and the ledger keeps both entries, append-only.

10. Deep dive: balances

Each shard has a leader for writes and followers for reads (the Replication lesson), a moment behind. Ana's own balance is read from her shard's leader: read-your-writes, from the CAP and Consistency lesson. A follower could still say $100 right after she sent $30, and she might send it again. The balance check inside a transfer always runs on the leader. Money is where you choose consistency over availability: if a shard's leader can't be reached, refuse the transfer rather than guess.

Each night a job checks every balance against its ledger entries, the way the payment system's reconciliation checks against the provider:

account   entries                 sum    balance
Ben       +$20 top-up, +$30 t1    $50    $80    mismatch
Gus       +$60 top-up, −$15 t5    $45    $45
Hal       +$100 top-up, +$20 t6   $120   $120

Ben's balance has $30 that no movement explains, say from a bug that added a retried credit to the balance outside the transaction that records it. The ledger is the truth: freeze the account, alert someone, fix the code, then set the balance from the entries. Across all shards, every entry plus the money in flight sums to zero.

What breaks first

  • A hot account: a shop paid by thousands of users at once locks its one row for each transfer; split it into several sub-accounts and add them up.
  • A shard's leader: it has the only up-to-date copy of its balances; keep a follower in another zone and fail over to it.
  • The transfer queue: when workers fall behind, money stays in flight longer; alert on the age of the oldest pending transfer.

Summary: the order to follow

  1. Scope: balances, top-ups, instant transfers, history; no money lost or made up, none below $0, each transfer once.
  2. Data: accounts with balances, an append-only double-entry ledger, transfers with a status and a key.
  3. API: POST /transfers with an idempotency key and the sender from the token; the balance; history by cursor.
  4. Busy path: key first, then the check, both balances and both entries in one transaction.
  5. Numbers: about 17,000 transfers and 85,000 writes a second at the peak.
  6. Services: the wallet moves money between users; the payment system brings it in.
  7. Scale: 20 shards by a hash of the user id; 19 transfers in 20 cross shards.
  8. Deep dives: on one shard, lock both rows in id order; across shards, a saga (debit and pending together, credit once by transfer id, undo if it never lands); your own balance from the leader; a nightly check against the ledger.

The same pieces fit anything that keeps money or points inside one system: gift cards, loyalty points, in-game currency.

Keep going with system designInteractive lessons from the building blocks to Twitter, Uber and payments, the way interviews ask. Free to start.
Start learning free