Design a Digital Wallet System
1. Problem Statement & Scope Clarification
System Mission
Design a bank-grade, horizontally scalable, globally distributed digital wallet backend platform (equivalent to PayPal, Cash App, or Venmo). The system must orchestrate high-throughput peer-to-peer (P2P) balance transfers, fiat deposits and withdrawals, locked multi-currency foreign exchange (FX), and sub-5ms balance queries. The core ledger must mathematically eliminate transaction deadlocks, prevent overdraft race conditions, and guarantee absolute financial balance conservation () across multi-master or sharded database clusters.
Digital Wallet Platform at a Glance
| Measure | Value |
|---|---|
| Registered Wallets | 50 Million |
| Daily P2P Transfers | 20 Million |
| Balance Invariant | Balance >=0 |
| Peak Transfer Throughput | 5,000 TPS |
| Scale Ceiling | 20,000 TPS |
| Ledger Invariant | ΣDebit = ΣCredit |
| Balance Query Latency | P99 < 5ms |
| Transfer Latency | P99 < 20ms |
| Deadlock Prevention | Lexicographic lock order |
| Primary DB | Aurora PostgreSQL Shard |
| Fast Cache | ElastiCache Redis |
| CDC Stream | Debezium on MSK |
Functional Requirements
- Instant Peer-to-Peer (P2P) Transfer (
ExecuteTransfer): Atomically debit the sender's wallet and credit the receiver's wallet within a strict single ACID transaction boundary, supporting optional memo metadata and idempotent execution tokens. - Deposit & Withdrawal (
TopUp/Withdraw): Move funds between external banking rails (FedNow, ACH, SEPA, debit card networks) and the internal digital wallet system. - Double-Entry Immutable Accounting: Every transfer must generate balanced debit and credit entries; account balance snapshots are strictly reconciled against the append-only ledger transaction history.
- Guaranteed Multi-Currency FX Conversion: Execute cross-border transfers by locking real-time foreign exchange rates for a guaranteed 60-second window, executing debit in source currency and credit in destination currency.
- Ultra-Low Latency Balance Inquiry (
GetBalance): Provide sub-5 millisecond responses for available, reserved, and locked balances via version-fenced in-memory caches.
Non-Functional Requirements (SLAs & SLOs)
- Consistency: Strict ACID Serializability. Zero lost updates, zero negative balance overdrafts, zero dirty reads, and zero Phantom reads.
- Availability: ("five nines") uptime SLA for balance queries and P2P transfers.
- Latency (P99):
- Balance Inquiry: (served via Redis cache with monotonic version fencing).
- Intra-Shard P2P Transfer: (row lock, balance check, debit/credit append).
- Cross-Shard P2P Transfer (Two-Phase Saga): .
- Throughput & Scale: Support a baseline peak throughput of , engineered to scale to during high-volume cultural events (Super Bowl, Lunar New Year Red Envelopes, Black Friday).
- Auditability & Retention: tamper-evident audit logging with a 7-year regulatory retention lifecycle in immutable AWS S3 Object Lock storage.
2. Capacity & Scale Estimation (Back-of-the-Envelope Math)
User Scale & Transaction Traffic
- Total Registered Wallets: ().
- Daily Active Wallets (DAU): ().
- Daily P2P Transfers: .
- Average Transfer QPS:
- Peak Transfer QPS ( burst during peak hours & sporting events):
- Flash Sale Scale Ceiling: Dimensioned to scale out to across database shards.
- Balance Inquiry Read Traffic ( Read/Write Ratio):
Storage Footprint & Capacity Growth (7-Year Regulatory Retention)
Every P2P transfer creates three physical rows:
- Transaction Header: ID, idempotency key, source account, dest account, amount, status .
- Sender Ledger Line (Debit): Entry ID, transaction ID, account ID, direction, amount, balance after .
- Receiver Ledger Line (Credit): Entry ID, transaction ID, account ID, direction, amount, balance after .
- Index & Metadata Overhead: .
- Total Storage per Transfer: .
- With B-tree indexes () and Aurora 3-AZ physical replication:
In-Memory Working Set (ElastiCache Redis)
- Active Wallets in Cache: 10M DAU (Account ID, Balance Cents, Locked Cents, Version Number, Timestamp) .
- With Redis Hash metadata and cluster replication ( factor): , easily accommodated by a small multi-node Redis cluster (
cache.r6g.xlarge).
3. High-Level Architecture & AWS Component Mapping
The architecture implements a decoupled Read/Write Segregated Model: read traffic (balance inquiries) hits an in-memory Redis cache protected by monotonic version fencing, while write traffic (P2P transfers) executes against sharded Amazon Aurora PostgreSQL clusters with row-level pessimistic locking.
Synthesizing vector architecture diagram...
Follow the two kinds of request. Balance checks go from the wallet service to Redis in the "In-Memory Balance Cache" panel; each cached balance carries a version number, and an update is applied only if its version is newer, so a delayed or out-of-order update can never put an old balance back. Transfers go through the shard router to the "ACID Core Ledger Tier" panel, where account IDs decide which Aurora shard owns each wallet; rows are always locked in sorted ID order, so two opposite transfers (Alice to Bob, Bob to Alice) cannot deadlock. After commit, the "Transactional Outbox & Cache Invalidation Pipeline" panel streams each change from the WAL through Debezium into Kafka, where consumers update the Redis balance, archive the ledger in write-once S3, and push a notification. Money moves only inside ACID transactions; everything users see is derived from the committed log.
Data Flow Walkthrough
- Balance Inquiry (Fast Read Path): The client queries
GET /v1/wallets/{account_id}/balance. The API checks ElastiCache Redis. If hit, the cached balance and monotonicversionare returned in . If missed, it queries the Aurora read replica, populates Redis, and returns. - Transfer Ingress & Routing (Write Path): Alice transfers to Bob. The request arrives with an
Idempotency-Key. The service determines the database shards for Alice and Bob using consistent hashing onaccount_id:- Intra-Shard Transfer (Alice & Bob on Shard 1): Handled in a single, high-speed ACID transaction.
- Cross-Shard Transfer (Alice on Shard 1, Bob on Shard 2): Coordinated via a Two-Phase Saga with semantic reservations.
- Deadlock-Free Row Locking: The transaction executes
SELECT balance_cents FROM accounts WHERE account_id IN ('acc_alice', 'acc_bob') ORDER BY account_id ASC FOR UPDATE. Sorting account IDs lexicographically guarantees circular lock deadlocks are mathematically impossible. - Balance Verification & Double-Entry Commit: The engine verifies Alice has sufficient funds (
balance_cents >= 2500), decrements Alice's balance, increments Bob's balance, increments both account version counters by 1, and appends balanced debit/credit lines intoledger_entries. - Change Data Capture & Async Invalidation: PostgreSQL WAL emits the commit via Debezium CDC to Amazon MSK. An ECS worker consumes the event and evicts or atomically updates the Redis cache using monotonic version checks (
NEW_VERSION > OLD_VERSION). Bob receives an instant push notification via Amazon SNS.
Core Request Tracing Execution Walkthrough
| Step # | Event / Action | Component State | Distributed Transition | Output / Response |
|---|---|---|---|---|
| Step 1 | Alice initiates P2P transfer POST /v1/wallets/transfers | ALB Ingress Fleet | Consistent hash ring maps acc_alice_101 and acc_bob_202 to Shard 1 | Authenticated request routed to primary database shard |
| Step 2 | Insert transaction header record with unique idempotency token | Aurora Shard 1 Primary Writer | INSERT INTO transactions ... ON CONFLICT (idempotency_key) DO NOTHING | Transaction tx_p2p_771829384 staged with status='PENDING' |
| Step 3 | Acquire pessimistic row-level locks on sender and receiver accounts | Relational Row Lock Manager | SELECT ... FOR UPDATE with ORDER BY account_id ASC (acc_alice then acc_bob) | Rows locked in linear sequence; circular deadlocks eliminated () |
| Step 4 | Validate account statuses and balance sufficiency | In-memory evaluation within active SQL TX | Verify status == 'ACTIVE' and available balance balance - locked >= 2500 | Pre-conditions verified; approved for atomic ledger execution |
| Step 5 | Mutate account balances and append immutable ledger entries | Aurora PostgreSQL ACID Engine | Alice , Bob ; bump version += 1; insert balanced Debit/Credit rows | Status COMMITTED () |
| Step 6 | WAL streaming captures commit and triggers cache eviction | Debezium CDC Amazon MSK | Worker executes atomic version-fenced delete on Redis keys | Monotonic version coordinate version=43 committed to client |
| Step 7 | Async event dispatched to mobile push notification gateway | Amazon MSK Amazon SNS | SNS publishes push notification payload to Apple APNs / Google FCM | Bob receives instant push: "You received $25.00 from Alice!" |
4. Double-Entry Accounting Model & Ledger Schema
Relational PostgreSQL DDL Schema (Aurora Multi-AZ)
sql-- 1. Accounts Master Table (Stores Current Balance Snapshot, Striping & Monotonic Version) CREATE TABLE accounts ( account_id VARCHAR(64) PRIMARY KEY, -- e.g. "acc_usr_998124" or "acc_merchant_nike_stripe_12" user_id VARCHAR(64) NOT NULL, parent_account_id VARCHAR(64), -- Set for striped sub-accounts (e.g., hot merchant / payroll wallets) stripe_id INT NOT NULL DEFAULT 0, -- Stripe index [0..N-1] for partitioned hot accounts currency CHAR(3) NOT NULL DEFAULT 'USD', balance_cents BIGINT NOT NULL DEFAULT 0, locked_cents BIGINT NOT NULL DEFAULT 0, -- Funds held for pending cross-shard sagas / card authorizations version BIGINT NOT NULL DEFAULT 1, -- Monotonic fencing version for cache synchronization & read fencing status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'FROZEN', 'CLOSED')), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_balance_non_negative CHECK (balance_cents >= 0), CONSTRAINT chk_locked_non_negative CHECK (locked_cents >= 0), CONSTRAINT chk_available_balance CHECK (balance_cents >= locked_cents) ); CREATE INDEX idx_accounts_parent_stripe ON accounts(parent_account_id, stripe_id) WHERE parent_account_id IS NOT NULL; -- 2. Transaction Headers Table CREATE TABLE transactions ( transaction_id VARCHAR(64) PRIMARY KEY, idempotency_key VARCHAR(128) UNIQUE NOT NULL, source_account_id VARCHAR(64) NOT NULL REFERENCES accounts(account_id), destination_account_id VARCHAR(64) NOT NULL REFERENCES accounts(account_id), amount_cents BIGINT NOT NULL CHECK (amount_cents > 0), currency CHAR(3) NOT NULL, type VARCHAR(32) NOT NULL CHECK (type IN ('P2P_TRANSFER', 'DEPOSIT', 'WITHDRAWAL', 'FX_EXCHANGE', 'COMPENSATION')), status VARCHAR(16) NOT NULL CHECK (status IN ('PENDING', 'COMMITTED', 'REJECTED', 'CANCELLED_REFUNDED')), memo VARCHAR(255), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 3. Immutable Double-Entry Ledger Lines Table (Append-Only Audit Trail) CREATE TABLE ledger_entries ( entry_id BIGSERIAL PRIMARY KEY, transaction_id VARCHAR(64) NOT NULL REFERENCES transactions(transaction_id), account_id VARCHAR(64) NOT NULL REFERENCES accounts(account_id), direction VARCHAR(6) NOT NULL CHECK (direction IN ('DEBIT', 'CREDIT')), amount_cents BIGINT NOT NULL CHECK (amount_cents > 0), currency CHAR(3) NOT NULL, balance_after_cents BIGINT NOT NULL, -- Point-in-time balance snapshot for instant auditing created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 4. Transactional Outbox Events (CDC & Cross-Shard Saga Dispatch) CREATE TABLE outbox_events ( event_id BIGSERIAL PRIMARY KEY, aggregate_type VARCHAR(32) NOT NULL DEFAULT 'WALLET_TRANSFER', aggregate_id VARCHAR(64) NOT NULL, -- transaction_id event_type VARCHAR(64) NOT NULL, -- 'TRANSFER_RESERVED', 'TRANSFER_COMPENSATED' payload JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Compound Indexes for Fast Queries & Settlement Audits CREATE INDEX idx_ledger_account_history ON ledger_entries(account_id, created_at DESC, entry_id DESC); CREATE INDEX idx_tx_idempotency ON transactions(idempotency_key); CREATE INDEX idx_outbox_unprocessed ON outbox_events(event_id ASC);
5. API Interface Design & Data Contracts
1. Execute P2P Balance Transfer (POST /v1/wallets/transfers)
Request Headers
httpPOST /v1/wallets/transfers HTTP/1.1 Host: api.wallet.platform.aws.internal Authorization: Bearer sec_tok_live_09182374 Idempotency-Key: idemp_p2p_99a8b7c6-d5e4-4a3b-b2c1 Content-Type: application/json
Request Payload
json{ "source_account_id": "acc_alice_101", "destination_account_id": "acc_bob_202", "amount_cents": 2500, "currency": "USD", "memo": "Dinner split from Friday tacos" }
Response: 200 OK
json{ "transaction_id": "tx_p2p_771829384", "status": "COMMITTED", "source_account_id": "acc_alice_101", "destination_account_id": "acc_bob_202", "amount_cents": 2500, "currency": "USD", "source_remaining_balance_cents": 12500, "idempotency_key": "idemp_p2p_99a8b7c6-d5e4-4a3b-b2c1", "created_at": "2026-09-16T14:45:10.112Z" }
2. Balance Inquiry with Monotonic Fencing (GET /v1/wallets/{account_id}/balance)
httpGET /v1/wallets/acc_alice_101/balance HTTP/1.1 Host: api.wallet.platform.aws.internal Authorization: Bearer sec_tok_live_09182374
Response: 200 OK
json{ "account_id": "acc_alice_101", "currency": "USD", "total_balance_cents": 12500, "locked_cents": 0, "available_balance_cents": 12500, "version": 42, "last_updated": "2026-09-16T14:45:10.112Z" }
Unlock Complete Architecture & Production Runbooks
You have explored the free architectural preview (~33%). Spend 1 Coin to unlock the remaining 6 production deep-dive sections for a full 24 hours.