Design a payment system for a mobile wallet

Money systems must never lose or double-count a taka. We design a wallet transfer with a ledger, idempotency and reconciliation.

Two wallets exchanging money through a ledger

This is a general design for a wallet-style system (users send money to each other). It is not a description of any specific company's internals.

Goal: transfer money between accounts correctly, safely and traceably.

Requirements

  • Transfer from account A to account B.
  • Never lose money, create money or apply a transfer twice.
  • Full audit trail; support refunds and disputes.
  • High availability, but correctness comes before speed.

Core: a double-entry ledger

Don't just store a balance and change it. Record ledger entries: every transfer creates a debit on one account and a credit on another, for the same amount, in one transaction. The sum of all entries is always zero, which makes errors detectable. A balance is the result of the entries (often cached as a running balance).

transfers(id, from, to, amount, status, idempotency_key) ledger_entries(id, transfer_id, account_id, amount, direction, created_at)

Consistency

Use a relational database with ACID transactions for the money path. In one transaction: lock both accounts (in a fixed order to avoid deadlocks), check sufficient balance, write the entries, update balances. Don't spread a single transfer across eventually consistent stores.

Idempotency

A client may retry after a timeout. Every request carries an idempotency key, stored with a unique constraint. A repeat returns the original result instead of paying twice.

Transfer flow

  1. Validate the request, authenticate, check limits and fraud rules.
  2. Create the transfer as PENDING (with the idempotency key).
  3. Execute the debit and credit in one transaction.
  4. Mark COMPLETED, then send notifications asynchronously.

External systems (banks, cards)

Calls to outside providers can time out with unknown results. Use a state machine, never assume failure, and later query the provider to learn the true outcome. Use sagas with compensations when several systems are involved.

Reconciliation

Every day, compare your ledger with bank and partner statements. Mismatches are investigated. This is how real systems catch bugs and fraud.

Security and risk

Strong authentication (PIN, OTP, device binding), encryption, rate limits, fraud detection (velocity checks, unusual amounts), audit logs and strict access control. Keep sensitive data encrypted and minimise who can see it.

Scaling

Shard by account or region, while keeping a single transfer's two accounts consistent (for example by routing or a coordinated two-step with a reliable ledger).

Takeaway: use a double-entry ledger in ACID transactions, make every request idempotent, treat unknown outcomes carefully, and reconcile daily.


System Design series, Part 3 (case studies): 7 of 16. Previous: Design a ride-sharing service like Uber.

Keep reading