Design (LLD) Paytm/Phone Wallet System - Machine Coding
Asked in Phonepe interview.
Design and implement a Paytm/PhonePe Wallet System that supports multiple concurrent users performing transactions at the same time. Your system should:
✅ Features Required
User & Wallet Management
Each user has a unique wallet with balance.
Support wallet creation and balance inquiry.
Concurrent Transactions
Add money to wallet (via UPI, Card, NetBanking).
Send money from one wallet to another.
Handle concurrent deposits and transfers without race conditions.
Transaction Lifecycle
Each transaction can be
PENDING → SUCCESS / FAILED / REVERSED.Ensure atomic debit/credit (no money disappears or duplicates even under concurrency).
Transaction Reliability
Idempotency (retries should not cause double debit).
Refund logic (if debit succeeds but credit fails, rollback debit).
Transaction History
Maintain mini-statement per user.
Show in descending order of time (latest first).
Solution approaches
🔑 Design Patterns to Use
Strategy Pattern → Different funding sources (UPI/Card/NetBanking).
Why Strategy?
Each payment source (UPI, Card, NetBanking) has the same high-level goal (fund wallet) but different implementation.
We want to swap/add new methods without modifying existing code.
Why not Adapter?
Adapter is for integrating third-party APIs with different interfaces.
Here, we control the implementation; we don’t need to adapt legacy/external code.
Why not Bridge?
- Bridge separates abstraction from implementation. Overkill here because abstraction (payment) and implementation (different sources) are already cleanly separated.
Template Method Pattern → Standard transaction workflow (Validate → Debit → Credit → Notify).
Why Template?
All transactions (add money, transfer, refund) follow the same skeleton: validate → debit/credit → update status → notify.
Only some steps differ (refund logic vs transfer logic).
Why not Chain of Responsibility?
- Chain is for passing requests along handlers dynamically. But our transaction flow is strict and sequential, not optional.
Why not Command Pattern?
- Command encapsulates actions as objects (undo/redo). We need structured workflow, not undo commands.
Factory Pattern → Create the right
Transactionobject.Why Factory?
We need to create
AddMoneyTransaction,SendMoneyTransaction, orRefundTransactiondynamically.Centralized creation avoids
if-elsescattered everywhere.
Why not Builder?
- Builder is for stepwise object construction (useful for complex objects). Our transaction objects are straightforward.
Why not Abstract Factory?
- Abstract Factory creates families of related objects. Here, only one family exists (transactions).
Observer Pattern → Notify users on transaction completion.
Why Observer?
After a transaction, multiple services (SMS, Email, Push) need notification.
Observer decouples transaction logic from notification system.
Why not Mediator?
- Mediator centralizes communication between objects, but here we don’t want a central coordinator, we want one-to-many updates.
Why not Publisher-Subscriber with Message Queue?
- That would be a distributed solution. For in-process design, Observer is sufficient.
State Pattern → Handle transaction states (
PENDING,SUCCESS, etc.).Why State?
A transaction can move from
PENDING → SUCCESS, orPENDING → FAILED, etc.Each state has different rules (e.g., SUCCESS cannot go back to PENDING).
Why not Strategy?
- Strategy is for choosing different algorithms. Here, we need to model object behavior change over time.
Why not Enum with Switch Case?
- Enums are fine for simple state. But extensibility suffers when adding new states or behaviors. State Pattern is more scalable.
Singleton Pattern → TransactionManager (to avoid inconsistent transaction coordination).
Why Singleton?
- TransactionManager should coordinate transactions globally. Multiple instances would risk inconsistent tracking.
Why not Static Class?
- Static class prevents dependency injection/testing. Singleton provides controlled access + lazy initialization.
Why not Monostate?
- Monostate shares state across instances, but still allows multiple objects. For a central coordinator, true Singleton is cleaner.
🔥 Concurrency Challenges to Handle
Race Condition: Two concurrent debits must not allow balance to go negative.
Deadlock Avoidance: If two users send money to each other simultaneously, system should not deadlock.
Atomicity: Debit and credit should be all-or-nothing.
Thread-Safe Wallets: Ensure balance updates are synchronized correctly.
⚡ Concurrency Choices
Fine-Grained Locking per Wallet
Avoids blocking all transactions globally.
Why not Global Lock? → Poor scalability; one user blocks all.
Consistent Lock Ordering (by UserId)
Prevents deadlock when two users transfer to each other.
Why not Random Locking? → Leads to deadlocks.
Optimistic Concurrency (Version Check)
- Not chosen here because wallet balances are critical → safer with pessimistic locks.
⚡ Algorithms Involved
Concurrency Control Algorithm
Use fine-grained locks per wallet instead of global locks for scalability.
Always acquire locks in a consistent order (lower userId first) to avoid deadlocks.
Idempotency Algorithm
Maintain a transactionId → status map.
If the same request is retried, return previous result.
Rollback Algorithm
- If debit succeeds but credit fails, perform rollback.
Mini-Statement Algorithm
- Use a priority queue / sorted list for recent transactions.
