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.

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
- Validate the request, authenticate, check limits and fraud rules.
- Create the transfer as
PENDING(with the idempotency key). - Execute the debit and credit in one transaction.
- 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.


