Perfect.
This is where everything you’ve learned finally comes together into a REAL banking-grade design.Today we’ll cover:
Ledger-based system design — how banks actually store and protect money
No fluff. This is core financial system architecture.
? LEDGER-BASED SYSTEM DESIGN
1️⃣ The Biggest Truth (Read This Twice)
Banks do NOT trust balances.
They trust ledgers.Balances are derived, ledgers are source of truth.
2️⃣ Why Storing Only Balance Is Dangerous
❌ Balance-only table
account ( id BIGINT PRIMARY KEY, balance BIGINT )Problems
- No history
- No audit
- No rollback
- No fraud detection
- Impossible reconciliation
❌ Unacceptable for finance
3️⃣ What Is a Ledger?
An append-only, immutable record of every financial event
Ledger rows are:
- Never updated
- Never deleted
- Time-ordered
4️⃣ Core Tables (Minimal Bank Design)
? Account
account (
id BIGINT PRIMARY KEY,
current_balance BIGINT NOT NULL
)
? Ledger (THE TRUTH)
ledger_entry (
txn_id UUID PRIMARY KEY,
account_id BIGINT,
type VARCHAR(10), -- DEBIT / CREDIT
amount BIGINT,
created_at TIMESTAMP
)
5️⃣ How a Deposit Works (Step by Step)
1. Insert ledger entry (CREDIT)
2. Update account balance atomically
3. Commit
Code
@Transactional
public void deposit(UUID txnId, Long accountId, int amount) {
ledgerRepo.insert(txnId, accountId, "CREDIT", amount);
accountRepo.addBalance(accountId, amount);
}
6️⃣ Withdraw Works the Same (But Guarded)
@Transactional
public void withdraw(UUID txnId, Long accountId, int amount) {
ledgerRepo.insert(txnId, accountId, "DEBIT", amount);
int rows = accountRepo.subtractBalance(accountId, amount);
if (rows == 0) {
throw new InsufficientFundsException();
}
}
✔ Ledger is always correct
✔ Balance is validated
7️⃣ Transfers (Two Ledgers, Two Accounts)
@Transactional
public void transfer(UUID txnId, Long fromId, Long toId, int amount) {
ledgerRepo.insert(txnId, fromId, "DEBIT", amount);
ledgerRepo.insert(txnId, toId, "CREDIT", amount);
accountRepo.subtractBalance(fromId, amount);
accountRepo.addBalance(toId, amount);
}
⚠ Ordered locking or atomic updates still apply.
8️⃣ Idempotency at Ledger Level (CRITICAL)
UNIQUE(txn_id, account_id)
Retry happens?
- Ledger insert fails
- Balance update not re-applied
- System stays correct
9️⃣ Why Ledger Must Be Immutable
| Reason | Why |
|---|---|
| Audit | Regulators |
| Debug | Recompute balance |
| Recovery | Rebuild state |
| Fraud | Tamper detection |
Never update a ledger row. Ever.
? How Balance Is Treated
Balance = cached aggregate
It exists for:
- Fast reads
- UI display
- Limits checking
But it is reconstructible:
SELECT SUM(
CASE WHEN type='CREDIT' THEN amount ELSE -amount END
)
FROM ledger_entry
WHERE account_id = ?;
1️⃣1️⃣ Handling Failures (Production)
If balance update fails?
Transaction rolls back → ledger not written
If service crashes?
Transaction rollback → nothing persisted
If retry happens?
Idempotency key prevents duplication
1️⃣2️⃣ Performance at Scale
Banks:
- Partition ledger by account_id
- Archive old entries
- Snapshot balances
- Async projections
But design remains same.
? Mental Model (Remember Forever)
Ledger = truth
Balance = convenience
✅ Summary
✔ Immutable ledger
✔ Atomic balance updates
✔ Idempotent transactions
✔ Auditable system
✔ Production-grade design
? You’ve Reached a Milestone
You now understand:
- How money is safely stored
- Why balances lie
- How real systems work
This is not tutorial knowledge — this is industry knowledge.
? Next (Advanced & Final)
Failure Handling, Retries, and Exactly-Once Processing