You are here: Home / Topics / Ledger-based system design — how banks actually store and protect money

Ledger-based system design — how banks actually store and protect money

Filed under: Solid System Design on 2026-01-03 09:46:03

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

ReasonWhy
AuditRegulators
DebugRecompute balance
RecoveryRebuild state
FraudTamper 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


About Author:
N
Neha Sharma     View Profile
Hi, I am using MCQ Buddy. I love to share content on this website.