Design a Digital Wallet
Design a digital wallet with balance management, P2P transfers, transaction history, and fraud detection.
Problem Statement
Design a digital wallet system (like PayPal or Venmo) that allows users to add funds from bank accounts, maintain a wallet balance, send money to other users (P2P transfers), pay merchants, and view transaction history. Must guarantee no money is created or lost, handle concurrent transactions, and detect fraud.
Requirements
Functional
- Add funds to wallet from linked bank account (ACH/bank transfer)
- P2P transfer: send money to another user by email, phone, or username
- Pay merchants via QR code or in-app checkout
- Transaction history with search and filtering (date range, type, amount)
Non-Functional
- Consistency: Zero tolerance for balance discrepancy -- double-entry bookkeeping
- Latency: P2P transfer completes in <2 seconds for wallet-to-wallet
- Scale: 100M users, 50M transactions/day, 5K TPS at peak
- Security: PCI-DSS compliant, all financial data encrypted at rest and in transit
Core Architecture
-
Ledger Service (Double-Entry Bookkeeping) -- Every money movement creates two ledger entries: a debit from one account and a credit to another. For P2P transfer: debit sender's wallet, credit receiver's wallet. Both entries are written atomically in a single database transaction. The sum of all debits always equals the sum of all credits -- enabling audit reconciliation.
-
Balance Service -- Maintains the current balance per wallet as a materialized view of the ledger. Balance = sum(credits) - sum(debits) for the wallet. Cached in Redis for fast reads. On each transaction, the balance is updated atomically with the ledger write using a CHECK constraint: balance >= 0 (prevents overdraft).
-
Payment Gateway Integration -- Handles add-funds (bank -> wallet) and cash-out (wallet -> bank) via ACH/wire. ACH transfers are asynchronous (2-3 business days). During this window, the wallet shows "pending" funds. Uses webhooks from the bank partner to confirm settlement. Failed ACH transfers trigger a clawback from the wallet balance.
- Fraud Detection Engine -- Real-time scoring on every transaction: velocity checks (>5 transactions in 1 minute), unusual amount (10x typical), new device/location, recipient is a flagged account. High-risk transactions are held for step-up verification (PIN, biometric). Uses an ML model trained on historical fraud patterns, updated weekly.
Database Choice
PostgreSQL for the ledger and balance tables -- ACID transactions essential for financial integrity. The ledger table is append-only (immutable audit trail). Balance updates use UPDATE wallets SET balance = balance - amount WHERE user_id = ? AND balance >= amount within the same transaction as ledger writes. Redis for cached balances (read-heavy: checking balance before every purchase) and rate limiting. Kafka for transaction events consumed by fraud detection, notifications, and analytics.
Key API Endpoints
POST /api/v1/transfers
-> Body: \{ sender_id: "U1", recipient: "[email protected]", amount: 50.00, idempotency_key: "uuid-abc" \}
-> Returns: \{ transfer_id: "TXN-789", status: "COMPLETED", sender_balance: 150.00 \}
POST /api/v1/wallet/add-funds
-> Body: \{ user_id: "U1", amount: 200.00, bank_account_id: "BA-3" \}
-> Returns: \{ transfer_id: "TXN-790", status: "PENDING", estimated_completion: "2024-01-18" \}
GET /api/v1/wallet/\{user_id\}/transactions?from=2024-01-01&to=2024-01-31&type=p2p
-> Returns: \{ transactions: [\{ id, type, amount, counterparty, timestamp, status \}], balance: 350.00 \}
Scaling Insight
Double-entry bookkeeping with immutable ledger entries is both the correctness mechanism and the scaling enabler. Because ledger entries are append-only (never updated or deleted), the table is trivially partitioned by time range and the write pattern is sequential. The balance is just a cached materialized view that can be recomputed from the ledger at any time. This means: no UPDATE contention on a "balance" row (only INSERT to ledger + single atomic balance UPDATE), full audit trail for reconciliation, and the ability to shard the ledger by user_id across PostgreSQL partitions.
Key Tradeoffs
| Decision | Option A | Option B | Chosen |
|---|---|---|---|
| Balance tracking | Compute from ledger on every read | Cached balance + ledger audit | Cached balance -- O(1) reads; ledger used for nightly reconciliation |
| Transfer model | Synchronous (block until complete) | Async with eventual consistency | Synchronous for wallet-to-wallet -- users expect instant confirmation |
| Fraud detection | Post-transaction (less latency) | Pre-transaction (blocks fraud) | Pre-transaction -- adds 50ms latency but prevents fraudulent transfers from completing |
Practical Implementation for .NET Developers
In a .NET application, you would typically implement this pattern using the following approach:
ASP.NET Core setup: Create a service class that encapsulates the logic, register it with dependency injection, and inject it into your controllers or minimal API endpoints. The built-in DI container handles lifecycle management.
Entity Framework Core: For database interactions, EF Core provides the ORM layer. Use migrations for schema management and raw SQL for performance-critical queries. Consider Dapper for read-heavy paths where EF Core's overhead matters.
Azure integration: If deploying to Azure, leverage managed services — Azure Cache for Redis, Azure SQL, Azure Service Bus, Azure Cosmos DB. These eliminate operational overhead and provide built-in monitoring through Application Insights.
Testing: Use xUnit with Testcontainers for integration tests that spin up real databases in Docker. Mock external dependencies with NSubstitute. The WebApplicationFactory class lets you test your entire HTTP pipeline in-process.
Monitoring: Add Application Insights telemetry to track request latency, dependency calls, and custom metrics. Use structured logging with Serilog to make production debugging possible:
Log.Information("Processing order {OrderId} for {CustomerId}", orderId, customerId);
This gives you searchable, structured logs in Azure Monitor or Seq.
System-Specific Clarifying Questions
Before designing Digital Wallet, ask questions specific to THIS system:
- Who are the primary users? Understanding the user base shapes every technical decision — consumer apps have different requirements than enterprise B2B systems.
- What is the read-to-write ratio? This determines whether you optimize for fast reads (caching, denormalization) or fast writes (write-ahead logs, async processing).
- What is the geographic distribution? Users in one country vs. global users fundamentally changes your data replication and CDN strategy.
- What is the acceptable latency? Some features need sub-100ms responses, others can tolerate seconds. This determines your caching and architecture strategy.
- What is the consistency requirement? Some data (payments, inventory) needs strong consistency. Other data (social feeds, recommendations) can be eventually consistent.
Architecture Deep Dive
The architecture for Digital Wallet should be designed around the specific access patterns of the system. Do not apply generic templates — every system has unique hotspots, bottlenecks, and scaling challenges.
Write Path: How does data enter the system? Is it bursty (event-driven, flash sales) or steady (sensor data, logs)? Bursty writes need queuing and backpressure. Steady writes can go directly to the database.
Read Path: How is data consumed? Is it fan-out (one write, many reads like social feeds) or point lookups (one read for specific data like user profiles)? Fan-out reads benefit from pre-computation and caching. Point lookups benefit from efficient indexing.
Hot Spots: Where are the bottlenecks? For Digital Wallet, identify the component that will fail first under load and design mitigation strategies: caching, sharding, rate limiting, or async processing.