Design a Digital Wallet (System Design Interview)
A double-entry ledger, transfers across shards with two-phase commit vs saga, the transactional outbox, idempotent apply for exactly-once, event sourcing…
Designing a digital wallet: common questions
How do you transfer money between accounts on different shards?
Commonly with a saga: debit the sender and credit an internal in-flight account in one local transaction with an outbox event, then credit the receiver idempotently on its shard. Two-phase commit also works but blocks accounts if the coordinator fails.
Why use a ledger instead of a balance column?
A ledger records every change as an immutable entry, so any balance can be recomputed and explained, and the books can be proven to sum to zero. Keep a cached balance updated in the same transaction for speed.
Related chapters
- Start here in System Design
- How a system design interview is graded
- The delivery framework: five phases and a clock
- Numbers to know: what one machine can really do
- System Design Concepts: CAP, SQL vs NoSQL, Message Queues
- SQL vs NoSQL: How to Choose a Database (System Design)
- Traffic Jumps 10×: What Breaks First? (System Design)
- Proxy vs Load Balancer vs API Gateway (System Design)
- CAP Theorem Explained Simply, with Examples
- Why Use a Message Queue? System Design Explained
- Message Queue vs Pub/Sub vs Kafka: Differences
- Polling vs Long Polling vs SSE vs Webhooks (System Design)
- API design in System Design
- REST vs GraphQL vs gRPC: Choosing an API (System Design)
- API Goals Canvas: Design APIs from the Consumer
- REST API Design: Resources, Methods, Status Codes
- HTTP Semantics, Richardson Maturity Model and HATEOAS
- GraphQL Explained: Over-fetching, Schema, N+1 Problem
- RPC and gRPC Explained: Protobuf, HTTP/2, When to Use
- API Design Best Practices: 9 Principles with Examples
- API Payload Design: Names, Types, Links and Errors
- Mapping Database Tables to API Resources, Step by Step
- API Design for Complex Schemas: Trees, Polymorphism, Bookings
- API Pagination, Idempotency Keys, Errors and Versioning
- Network-Efficient APIs: HTTP Caching, ETags, Field Selection
- Async APIs: 202 Accepted, Webhooks and Bulk Operations
- API Versioning and Evolution Without Breaking Clients
- API Contracts: OpenAPI, Contract Testing and Docs
- API Security: Authentication, Authorization, Rate Limiting
- OAuth 2, OpenID Connect and Scopes for APIs
- OWASP API Top 10 Explained: BOLA, SSRF and More
- Running APIs: Gateways, Mesh, Rate Limits, Canaries
- API Troubleshooting Interview: Slow APIs and Unexpected Errors
- API Design Review: A Staff-Level Capstone
- Spotify Web API: Resources, IDs, Payloads and Paging
- Spotify Web API: OAuth PKCE, Scopes, Quotas, Rate Limits
- Spotify Web API Changes: Real API Evolution Lessons
- Database-heavy designs in System Design
- Design a Distributed Key-Value Store (System Design Interview)
- Design a Hotel Reservation System (System Design Interview)
- Design S3-like Object Storage (System Design Interview)
- Games in System Design
- Shard City: Learn Database Sharding by Playing a Game
- System Design Case Studies: Flash Sale, Chat, ChatGPT
- Flash Sale System Design: Who Gets the Last Item
- Food Delivery App System Design: Which Database?
- Cache Stampede: Why the Database Fell Over and the Fix
- Live Streaming System Design: Scaling for a Final
- ChatGPT System Design: Streaming LLM Responses
- RAG System Design: Product Q&A on Sale Day
- IRCTC Tatkal System Design: Booking Under Extreme Load
- Group Chat System Design: WebSockets and Fan-Out
- Authentication in System Design: Sessions, JWT, OAuth
- Authentication Methods Compared: Sessions, JWT, OAuth
- JWT in System Design: Sessions, Revocation, Refresh Tokens
- JWT Interview Questions and Answers
- Life of a session, step by step
- JWT Structure Explained: Header, Payload, Signature
- Forge lab: why verification must be strict
- JWT vs Session Authentication: Which to Use
- The validation pipeline: where a bad request dies
- How to Revoke a JWT: Blocklists and Short Expiry
- Refresh Tokens and Rotation Explained
- Logout in depth: one device, all devices, SSO
- JWT Security Best Practices
- OAuth 2.0 and OpenID Connect Explained
- Where to Store JWT: localStorage vs HttpOnly Cookie
- JWT in Microservices: API Gateway vs Service Validation
- Microservices, keys, and trust boundaries
- Sender-constrained tokens: making a stolen token useless
- Authentication vs Authorization Middleware
- Failure explorer: what breaks when X fails
- Capacity planner: sessions vs JWT vs hybrid
- Edge cases catalog
- Test it, measure it, migrate to it
- Anti-patterns to reject in a design review
- Design checklist and cheat sheet
Keep learning
Overview
Two separate writes Two-phase commit Saga + in-flight account No failure Crash after debiting Asha Credit applied, reply lost, retry Shard 1: Asha −100, commit Shard 2: Ravi +100, commit Works — on a good day. Nothing ties the two commits together. Pick a failure to see why that matters. Service crashes Nobody remembers the credit is owed ₹100 has vanished. Asha was debited and Ravi was never credited. Total money in the system went from 1,000 to 900. Reply lost, timeout Retry: Ravi +100 again ₹100 was created out of nothing. The retry had no way to know the credit already happened. Total money went from 1,000 to 1,100. Coordinator: PREPARE both shards Both vote yes, rows locked Coordinator crashes before COMMIT Asha and Ravi frozen for 40 s (in doubt) Coordinator restarts, reads its log, COMMIT Correct in the end, but both accounts were blocked. No money lost, but every other payment touching Asha or Ravi waited on the locks. At wallet scale, a stuck coordinator becomes an outage. PREPARE both shards Both vote yes COMMIT both Atomic across shards. Shard 1 txn: Asha −100, In-flight +100, outbox event T81 Relay restarts, finds unsent T81 in outbox Shard 2 txn: Ravi +100, entry T81 (unique), In-flight −100 Recovered with no money lost. For a moment the ₹100 sat in the in-flight account, so the books always balanced. If the credit can never succeed (Ravi’s wallet closed), a compensating step refunds Asha. Shard 1: Asha −100, In-flight +100, outbox T81 Shard 2: Ravi +100 (entry T81) Ack lost, event T81 delivered again Shard 2: unique(transfer_id) → already applied, skip Duplicate ignored. Shard 2: Ravi +100, In-flight −100 Done in two local transactions. Each step is a normal single-shard transaction. No locks are held across the network. Lab · Asha (shard 1) sends ₹100 to Ravi (shard 2). Both start with ₹500. total money (should be ₹1,000) system design · database-heavy designs · digital wallet Design a digital wallet Users keep a balance and send money to each other, like Paytm, PhonePe’s wallet or Venmo. Speed matters, but one rule beats everything: money is never created or destroyed by a bug. Every design choice here is about keeping that rule true while the system is sharded, retried and crashing.
1. Requirements and numbers
Top up from a bank or card, transfer to another wallet, withdraw, see balance and history. No lost or duplicated transfers. Balances never go negative. Every rupee traceable to the entry that moved it. Regulators and support must be able to answer “why is my balance X?” for any past moment. 100 million wallets, 20,000 transfers a second at peak, p99 under 500 ms. transfers at peak ledger rows written (two per transfer) new ledger data a year at 100 bytes a row and 1 billion transfers a month A tuned single Postgres primary handles a few thousand such transactions a second. At 20,000 we will have to shard, and that is where it gets interesting.
2. The ledger: never store a balance you cannot explain
Use double-entry bookkeeping. Every transfer writes two ledger entries that sum to zero: −100 on Asha, +100 on Ravi. The balance is the sum of an account’s entries. Keep a cached balance column for speed, updated in the same transaction as the entries. On one database, a transfer is one transaction: Only a balance column Fast, but when a balance is wrong there is no record of how it got there. Support and audit are guesswork. Ledger + cached balance Every change is an immutable entry. Reconciliation can recompute any balance and prove the books sum to zero. Store money as integers in the smallest unit (paise, cents). Never floats.
3. Shard by account, and the transfer splits in two
Shard accounts and their ledger entries by account_id. Most reads (balance, history) now hit one shard. But Asha and Ravi usually live on different shards, so the transfer can no longer be one local transaction.
4. Choosing a cross-shard approach
No money lost No blocking on crash Two separate writes Two-phase commit Saga + in-flight account + outbox Event-sourced ledger The saga is the common answer. Step one debits the sender and credits an internal in-flight account in one local transaction, and writes an event to an outbox table in that same transaction. A relay publishes the event. Step two, on the receiver’s shard, credits the receiver and debits in-flight, keyed by transfer_id so a repeat is ignored. The in-flight account means the books balance at every instant, even mid-transfer. Sender balance checks happen in step one, so a transfer can still fail at step two only for reasons like a closed or frozen receiver. Then a compensating transfer moves the money from in-flight back to the sender. It is a new entry, never an edit: the ledger is append-only.
5. “Exactly once” is two ideas together
Idempotency-Key per Send tap unique key → same transfer row Outbox relay Receiver shard UNIQUE(transfer_id, account_id) Networks only promise at least once. Idempotent writes at every hop turn that into an effectively-once result.
6. Event sourcing: the ledger as the source of truth
Taken further, the ledger is not a side table but the system itself. Commands (“send ₹100”) are validated against current state and produce events (“debited”, “credited”). State is a fold over events, rebuilt from the latest snapshot plus newer events. You get a perfect audit trail and can replay history to fix a bug in a derived view, at the cost of more moving parts and careful versioning of event formats. Store state every N events so rebuilds start near the end. Read models History pages and analytics read projections built from the event stream (CQRS). Deterministic apply Applying the same events must always give the same state: no clocks or random IDs inside.
7. Operating it
Hot merchant accounts Limits and fraud Daily limits per wallet, velocity checks, and holds on suspicious transfers sit in front of the ledger, not inside it. Read replicas History can come from replicas. The balance check before a debit must use the primary.
8. What to say in the interview
Double-entry ledger with integer amounts and a cached balance updated in the same transaction. Guard the debit with a conditional update so the balance cannot go negative. Saga with an in-flight account, a transactional outbox, and idempotent apply on the receiver. Close with reconciliation, hot-account splitting and how support explains a balance. Related: the outbox pattern · two-phase commit, stuck · idempotency keys
More courses
- Jobs, linked to lessons — Browse jobs where every requirement links to the chapter that teaches it. See how ready you are, how long the gap takes to close, and which listings look stale.
- Match a job post to courses — Paste a job post. Each skill is matched to a track here and put in learning order, foundations first.
- React from open source with patterns — Patterns extracted from Material UI, Radix, React Aria, React Hook Form, Redux Toolkit, TanStack, and Next.js — not slides about hooks.
- Database in depth — How a query becomes work. Indexes, isolation, locks, and the plans that explain why a page is slow — not an ORM tour.
- Build a real API with NestJS — One ticket-booking API, built in order: a plain Node server first, then NestJS modules, validation, a real database, and the features real clients lean on.
- Microservices — Microservice patterns one diagram at a time: how to split a monolith, how services talk, and how data stays consistent without distributed transactions.
- Kafka — A beginner-to-senior path through event logs, reliable delivery, production operations, and disaster recovery.
- AI engineering for software engineers — Eight free reads, in order, for software engineers moving into AI: when to build an agent, how to build one, how to test it, and how the model works underneath.
- Frontend system design — How a UI is sliced when it is a product, not a page: ownership of state, cache, routes, and what happens when the network lies.
- TypeScript in depth — The type system as a design tool. Inference, narrowing, generics, and the errors that mean the model is wrong — not a catalog of utility types.
- CSS in depth — See the box, then the page. Visual chapters from the box model through Flexbox surprises, Grid, selectors, and units, then backgrounds, shapes, and visual effects, each with a playground.
- Accessibility — Foundations first (WCAG in plain words, semantic HTML, lists, alt text), then hands-on HTML labs, a full pass of WAI-ARIA 1.3, and a final fix-the-page challenge.
- Docker — What an image is, why layers cache, and how containers find each other on a network.
- Redis — Fifteen Redis patterns, one picture each: cache, session, lock, counter, rate limiter, queue, and when Redis is the wrong tool.
- Kubernetes — Kubernetes from zero to operators: why it exists, Pods, Deployments and Services, then configuration, networking, storage, scheduling, scaling and security.