Complete Interview Guide | Double-Entry Accounting | ACID Transactions | Production-Ready Architecture
Introduction
A Banking Ledger System is one of the most practical Low-Level Design (LLD) interview questions because it combines:
- Object-Oriented Design (OOD)
- Financial Domain Modeling
- ACID Transactions
- Double-Entry Accounting
- Concurrency Control
- Auditability
- Idempotency
- Event-Driven Architecture
Companies such as Amazon, Microsoft, Stripe, PayPal, Razorpay, PhonePe, Visa, Mastercard, and banks frequently discuss ledger concepts in interviews.
A ledger is the source of truth for all financial transactions. Account balances should be derived from ledger entries, not maintained as the primary source of truth.
Problem Statement
Design a Banking Ledger System that supports:
- Create accounts
- Credit money
- Debit money
- Transfer funds
- View transaction history
- Maintain account balances
- Audit every transaction
- Ensure atomicity and consistency
- Prevent duplicate processing (idempotency)
Functional Requirements
Core Features
- Create customer accounts
- Record deposits
- Record withdrawals
- Transfer between accounts
- Retrieve ledger history
- Query account balance
- Reverse transactions
- Idempotent transaction processing
Non-Functional Requirements
- ACID compliant
- Highly available
- Strong consistency
- Thread-safe
- Fault tolerant
- Immutable audit trail
- Horizontally scalable
Real-World Use Cases
- Bank accounts
- Digital wallets
- UPI transactions
- Credit card systems
- Payment gateways
- Trading platforms
- Payroll systems
- Expense management
Core Banking Principle: Double-Entry Accounting
Every financial transaction creates at least two ledger entries.
Transfer ₹1000
Alice Account -1000 (Debit)
Bob Account +1000 (Credit)
Total Change = 0
This guarantees:
- No money is created unexpectedly
- No money disappears unexpectedly
- Every movement is traceable
High-Level Architecture
Client
│
▼
Ledger Service
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Account Service Transaction Engine Ledger Store
│ │ │
▼ ▼ ▼
Account Repo Journal Entries Database
│
▼
Audit Log
Core Components
| Component | Responsibility |
|---|---|
| Account | Customer account |
| LedgerEntry | Immutable debit/credit record |
| Transaction | Groups ledger entries |
| LedgerService | Business operations |
| BalanceCalculator | Computes balances |
| AuditService | Tracks changes |
| TransactionRepository | Stores transactions |
Domain Model
+----------------------+
| Account |
+----------------------+
| accountId |
| customerId |
| status |
+----------+-----------+
|
|
1 | *
|
▼
+----------------------+
| LedgerEntry |
+----------------------+
| transactionId |
| accountId |
| amount |
| type |
| timestamp |
+----------+-----------+
|
|
▼
+----------------------+
| Transaction |
+----------------------+
| transactionId |
| status |
| referenceId |
+----------------------+
Design Patterns Used
| Pattern | Purpose |
|---|---|
| Strategy | Balance calculation, fee policies |
| Factory | Create transaction types |
| Builder | Construct immutable transactions |
| Command | Represent financial operations |
| State | Transaction lifecycle |
| Observer | Notifications and auditing |
| Repository | Data access abstraction |
| Singleton | Configuration services |
Enumerations
Entry Type
public enum EntryType {
DEBIT,
CREDIT
}
Transaction Status
public enum TransactionStatus {
PENDING,
SUCCESS,
FAILED,
REVERSED
}
Account
public class Account {
private final String accountId;
private final String customerId;
private AccountStatus status;
// getters
}
Ledger Entry
import java.math.BigDecimal;
import java.time.Instant;
public class LedgerEntry {
private final String transactionId;
private final String accountId;
private final EntryType type;
private final BigDecimal amount;
private final Instant timestamp;
public LedgerEntry(
String transactionId,
String accountId,
EntryType type,
BigDecimal amount) {
this.transactionId = transactionId;
this.accountId = accountId;
this.type = type;
this.amount = amount;
this.timestamp = Instant.now();
}
}
Transaction
import java.util.List;
public class Transaction {
private final String transactionId;
private final List<LedgerEntry> entries;
private TransactionStatus status;
private final String idempotencyKey;
public Transaction(
String transactionId,
List<LedgerEntry> entries,
String idempotencyKey) {
this.transactionId = transactionId;
this.entries = entries;
this.idempotencyKey = idempotencyKey;
this.status = TransactionStatus.PENDING;
}
}
Ledger Service
public interface LedgerService {
void deposit(String accountId, BigDecimal amount);
void withdraw(String accountId, BigDecimal amount);
void transfer(
String fromAccount,
String toAccount,
BigDecimal amount);
}
Transfer Flow
Client
│
▼
Validate Accounts
│
▼
Check Balance
│
▼
Create Transaction
│
▼
Create Debit Entry
│
▼
Create Credit Entry
│
▼
Persist Both Entries (Single DB Transaction)
│
▼
Commit
│
▼
Publish Event
Transfer Example
Transfer ₹500
Transaction ID : TX100
Ledger
--------------------------------
Alice
Debit
₹500
Bob
Credit
₹500
Balance Calculation
Recommended approach: derive the balance from immutable ledger entries.
Balance = Σ(Credits) − Σ(Debits)
For performance, many production systems maintain a cached/materialized balance that is derived from and periodically reconciled against the ledger.
Idempotency
Every external financial request should include an Idempotency Key.
POST /transfer
Idempotency-Key: abc123
If the same request is retried:
Already Processed
Return Previous Result
This prevents duplicate transfers caused by retries or network failures.
Database Schema
Account
| Column | Type |
|---|---|
| account_id | UUID |
| customer_id | UUID |
| status | ENUM |
Transaction
| Column | Type |
|---|---|
| transaction_id | UUID |
| status | ENUM |
| idempotency_key | VARCHAR (UNIQUE) |
| created_at | TIMESTAMP |
Ledger Entry
| Column | Type |
|---|---|
| entry_id | UUID |
| transaction_id | UUID |
| account_id | UUID |
| entry_type | ENUM |
| amount | DECIMAL |
| created_at | TIMESTAMP |
Concurrency Control
Use:
- Database transactions
- Optimistic locking (version columns)
- Pessimistic locking for high-contention accounts
- Consistent account lock ordering to avoid deadlocks
- Idempotency keys
- Retry with exponential backoff for transient conflicts
Thread Safety
Prefer:
- Stateless services
- ACID database transactions
- Immutable ledger entries
- Optimistic locking
Avoid:
- Updating balances without transaction boundaries
- Shared mutable in-memory state as the source of truth
Transaction Lifecycle
CREATED
│
▼
VALIDATED
│
▼
PENDING
│
┌────────┴────────┐
▼ ▼
SUCCESS FAILED
│
▼
REVERSED
Production Enhancements
- Event sourcing
- Outbox Pattern for reliable event publishing
- CDC (Change Data Capture)
- Multi-currency support
- Exchange rate service
- Scheduled reconciliation jobs
- Fraud detection
- AML/KYC integration
- Settlement engine
- Ledger snapshots
- Read replicas for reporting
- Encryption of sensitive data
- Comprehensive audit logs
SOLID Principles
| Principle | Application |
|---|---|
| Single Responsibility | Separate services for ledger, accounts, auditing, and balance calculation. |
| Open/Closed | New transaction or fee types can be added without modifying existing logic. |
| Liskov Substitution | Different repository implementations can be substituted transparently. |
| Interface Segregation | Small interfaces (LedgerService, AccountRepository, AuditService). |
| Dependency Inversion | Business logic depends on abstractions rather than database implementations. |
Complexity Analysis
| Operation | Complexity |
|---|---|
| Create Transaction | O(1) |
| Create Ledger Entries | O(1) |
| Transfer | O(1) (excluding I/O) |
| Balance Calculation | O(n) over ledger entries |
| Cached Balance Lookup | O(1) |
Common Interview Follow-up Questions
- Why use double-entry accounting instead of updating balances directly?
- How would you guarantee atomicity across debit and credit entries?
- How would you prevent duplicate transfers?
- How would you implement transaction reversal?
- How would you reconcile cached balances with the ledger?
- How would you support millions of transactions per day?
- How would you shard ledger data while preserving consistency?
- When would you use optimistic locking versus pessimistic locking?
- How would you design the ledger for multi-currency transactions?
- How would you integrate the ledger with Kafka using the Outbox Pattern?
Interview Tips
Explain the Difference Between Account Balance and Ledger
- Ledger: Immutable source of truth containing every financial movement.
- Balance: A derived or cached value computed from the ledger.
Emphasize Financial Correctness
- Double-entry accounting
- ACID transactions
- Idempotency
- Immutable audit trail
- Consistency over availability for core financial operations
Mention Production Considerations
- Event sourcing (where appropriate)
- Outbox Pattern + Kafka
- CDC for downstream systems
- Optimistic locking
- Reconciliation jobs
- Monitoring and alerting
- Disaster recovery and backups
Key Takeaways
- Model every financial operation as an immutable Transaction containing balanced Ledger Entries.
- Use double-entry accounting to ensure every debit has a corresponding credit.
- Treat the ledger as the source of truth; balances are derived or cached views.
- Ensure ACID transactions, idempotency, and auditability for every operation.
- Design for extensibility using Repository, Strategy, Factory, and Command patterns.
- Discuss concurrency, reconciliation, distributed messaging, and operational concerns to demonstrate senior-level system design expertise.
Top comments (0)