Banking System Design: A Complete Interview Guide

Before We Start: How to Use This Guide

This guide is written for two audiences:
  1. If you're preparing for interviews: Read everything. Understand the "why" behind each decision. Interviewers don't want you to memorize schemas—they want to see you think.
  2. If you're new to system design: Don't worry about understanding everything at once. Read it like a story. The concepts will click as you go.
My promise to you: By the end of this guide, you'll be able to walk into any interview and design a banking system from scratch, explaining every decision with confidence.

Chapter 1: Understanding What We're Building

What Does a Bank Actually Do?

Before we write a single line of code or draw a single table, let's understand what a bank does. This is where most interview candidates fail—they jump to solutions without understanding the problem.
A bank is essentially a trust machine. People give it money, trusting they'll get it back. The bank keeps track of who gave what, who took what, and makes sure the numbers always add up.
Let me break this down into simple operations:
Banking system diagram 1

Banking system diagram 1

Why Banking Systems Are Different

When you design Instagram and lose a photo, it's annoying. When you design a bank and lose money, it's a crime. This changes everything about how we design.
The Three Unbreakable Rules of Banking:
RuleWhat It MeansExample
Never Lose MoneyIf someone deposits ₹1000, it must exist forever until they withdraw itEven if servers crash, data centers burn, the money must be safe
Never Create MoneyMoney can only move, not appear from nowhereIf A sends ₹100 to B, A loses exactly ₹100 and B gains exactly ₹100
Never Show Wrong NumbersThe balance shown must always be accurateNo "eventual consistency" - you can't show ₹5000 when reality is ₹4000

Scale: How Big Is Banking DB?

Let's put some numbers to understand what we're designing for:
Banking system diagram 2

Banking system diagram 2

What these numbers mean for our design:
  • 10 million daily transactions = ~115 transactions per second on average
  • But on salary day (1st of month) = 1000+ transactions per second
  • During Diwali/festivals = Even higher peaks
  • We must design for the peak, not the average

Chapter 2: The Data - What Do We Need to Store?

Think Like a Bank Manager

Imagine you're a bank manager with a pen and notebook. What information would you write down?
For each customer, you'd write:
  • Their name, address, phone number
  • Their ID proof (PAN, Aadhaar)
  • When they joined the bank
  • Are they a good customer or risky?
For each account, you'd write:
  • Account number
  • Whose account is it?
  • How much money is in it?
  • What type of account (savings, current)?
  • Which branch handles it?
For each transaction, you'd write:
  • What happened (deposit, withdrawal, transfer)?
  • How much money?
  • When did it happen?
  • What was the balance before and after?
This is exactly what our database tables will store. Let's design them one by one.

Chapter 3: Database Tables - Every Table Explained

Table 1: Customers - The People

What is this table? This table stores information about every person who has a relationship with the bank.
Why do we need it? You can't have a bank account without knowing who owns it. This is the foundation.
Banking system diagram 3

Banking system diagram 3

Why each field exists:
FieldWhy We Need ItInterview Insight
customer_idComputer needs a fast way to find customers. Numbers are faster than names."We use BIGINT because we might have billions of customers over time"
customer_numberHumans need readable IDs. You can't tell a customer "Your ID is 847293847""This is for human interfaces, customer_id is for system interfaces"
pan_number (encrypted)Required by law. But we encrypt it because it's sensitive"We use AES-256 encryption. Even if database is stolen, data is safe"
aadhaar_hashWe need to verify identity but don't want to store actual Aadhaar"We only store a hash. We can verify but can't recover the actual number"
statusAccount might be frozen, closed, or active"This allows us to soft-delete customers without losing history"
kyc_statusRBI requires Know Your Customer verification"Unverified customers have transaction limits"
risk_categorySome customers need extra monitoring (politicians, etc.)"PEP = Politically Exposed Person. Extra scrutiny required by law"
The SQL:
sql
CREATE TABLE customers ( -- Primary identifier - the computer's way to find you customer_id BIGSERIAL PRIMARY KEY, -- Human-readable identifier - what you see on your passbook customer_number VARCHAR(20) NOT NULL UNIQUE, -- Basic information first_name VARCHAR(100) NOT NULL, middle_name VARCHAR(100), last_name VARCHAR(100) NOT NULL, date_of_birth DATE NOT NULL, -- Sensitive information (encrypted/hashed) pan_number_encrypted BYTEA, -- Encrypted, not plain text aadhaar_hash VARCHAR(64), -- Only hash, cannot reverse -- Contact (needed for OTP, alerts) phone_primary VARCHAR(15) NOT NULL, email VARCHAR(255), -- Status tracking status VARCHAR(20) DEFAULT 'ACTIVE', kyc_status VARCHAR(20) DEFAULT 'PENDING', risk_category VARCHAR(20) DEFAULT 'NORMAL', -- Audit trail (who created, when modified) created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Soft delete (never actually delete customer data) is_deleted BOOLEAN DEFAULT FALSE ); -- Why these indexes? -- Finding customer by phone (login, OTP verification) CREATE INDEX idx_customers_phone ON customers(phone_primary); -- Finding customer by name (branch staff search) CREATE INDEX idx_customers_name ON customers(last_name, first_name);
Interview Question: "Why don't you just delete customers when they close their account?"
Your Answer: "We never delete financial data. Regulatory requirements mandate keeping records for 10+ years. Also, a 'deleted' customer might have historical transactions we need for audits. Instead, we use soft delete - set is_deleted = true but keep the data."

Table 2: Accounts - Where Money Lives

What is this table? This table tracks every bank account. One customer can have multiple accounts (savings, fixed deposit, loan).
Why do we need it? Money doesn't belong to a person directly—it belongs to an account. The account is the container for money.
Banking system diagram 4

Banking system diagram 4

The Critical Concept: Balance vs Available Balance
This confuses many people. Let me explain with a real example:
You have ₹10,000 in your account. You deposit a cheque for ₹5,000. The cheque takes 2 days to clear. Balance: ₹15,000 (total money, including pending cheque) Available Balance: ₹10,000 (money you can actually withdraw TODAY) Hold Amount: ₹5,000 (the pending cheque) Formula: Available Balance = Balance - Hold Amount
Why the version field?
This is crucial for preventing race conditions. Imagine:
Time 0: Your balance is ₹1000 Time 1: ATM in Mumbai reads balance = ₹1000 Time 2: ATM in Delhi reads balance = ₹1000 Time 3: Mumbai ATM withdraws ₹800, sets balance = ₹200 Time 4: Delhi ATM withdraws ₹800, sets balance = ₹200 Problem: You withdrew ₹1600 from ₹1000 account!
The version field prevents this:
Time 0: Balance = ₹1000, Version = 1 Time 1: Mumbai reads: Balance = ₹1000, Version = 1 Time 2: Delhi reads: Balance = ₹1000, Version = 1 Time 3: Mumbai updates: "SET balance = 200 WHERE version = 1" Success! Version becomes 2 Time 4: Delhi updates: "SET balance = 200 WHERE version = 1" FAILS! Version is now 2, not 1 Delhi ATM says "Please try again"
The SQL:
sql
CREATE TABLE accounts ( -- Identifiers account_id BIGSERIAL PRIMARY KEY, account_number VARCHAR(20) NOT NULL UNIQUE, -- Relationships customer_id BIGINT NOT NULL REFERENCES customers(customer_id), branch_id INT NOT NULL REFERENCES branches(branch_id), account_type_id INT NOT NULL REFERENCES account_types(account_type_id), -- Money tracking (DECIMAL for exact precision - never use FLOAT for money!) balance DECIMAL(18,2) NOT NULL DEFAULT 0.00, available_balance DECIMAL(18,2) NOT NULL DEFAULT 0.00, hold_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00, minimum_balance DECIMAL(18,2) NOT NULL DEFAULT 0.00, -- Interest (for savings accounts) interest_rate DECIMAL(5,4), -- 0.0350 = 3.5% -- Limits (fraud prevention) daily_withdrawal_limit DECIMAL(18,2) DEFAULT 100000.00, daily_transfer_limit DECIMAL(18,2) DEFAULT 500000.00, -- Status status VARCHAR(20) DEFAULT 'ACTIVE', -- THE CRITICAL VERSION FIELD version INT NOT NULL DEFAULT 1, -- Timestamps opened_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Why DECIMAL(18,2)? -- 18 total digits, 2 after decimal -- Maximum: 9,999,999,999,999,999.99 (nearly 10 quadrillion) -- Enough for any bank balance, with paisa precision
Interview Question: "Why DECIMAL and not FLOAT for money?"
Your Answer: "FLOAT has precision issues. For example, 0.1 + 0.2 in floating point equals 0.30000000000000004, not 0.3. In banking, even 1 paisa error across millions of transactions becomes a huge problem. DECIMAL stores exact values."

Table 3: Transactions - The Money Movements

What is this table? Every time money moves—deposit, withdrawal, transfer, interest credit, fee debit—it's recorded here.
Why do we need it? This is your audit trail. If anyone asks "where did my money go?", this table has the answer.
Banking system diagram 5

Banking system diagram 5

Why We Store Balance Before and After:
This seems redundant, but it's gold for debugging and audits:
Transaction 1: Deposit ₹1000 balance_before: 0 balance_after: 1000 Transaction 2: Withdraw ₹500 balance_before: 1000 balance_after: 500 Transaction 3: Transfer ₹200 balance_before: 500 balance_after: 300
Benefits:
  1. Instant balance history: You can see balance at any point in time
  2. Error detection: If balance_after of Txn 2 ≠ balance_before of Txn 3, something is wrong
  3. Audit proof: Regulators can verify every rupee movement
Why We Partition This Table:
This table grows FAST. 10 million transactions per day = 3.65 billion per year.
sql
-- Partitioned by month CREATE TABLE transactions ( transaction_id BIGSERIAL, account_id BIGINT NOT NULL, amount DECIMAL(18,2) NOT NULL, direction CHAR(1) NOT NULL, -- 'C' or 'D' balance_before DECIMAL(18,2) NOT NULL, balance_after DECIMAL(18,2) NOT NULL, transaction_type VARCHAR(50) NOT NULL, channel VARCHAR(20) NOT NULL, status VARCHAR(20) DEFAULT 'COMPLETED', description VARCHAR(500), reference_number VARCHAR(50), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, ip_address INET, PRIMARY KEY (transaction_id, created_at) ) PARTITION BY RANGE (created_at); -- Create partitions for each month CREATE TABLE transactions_2024_01 PARTITION OF transactions FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); CREATE TABLE transactions_2024_02 PARTITION OF transactions FOR VALUES FROM ('2024-02-01') TO ('2024-03-01'); -- ... and so on
Why Partition?
Banking system diagram 6

Banking system diagram 6

Benefits of Partitioning:
BenefitExplanation
Faster queries"Show me January transactions" only scans January partition
Easy archivalDrop old partitions instead of deleting rows
Parallel processingDifferent partitions can be backed up simultaneously
Smaller indexesEach partition has its own smaller, faster indexes

Table 4: Ledger Entries - The Accountant's Double-Entry

What is this table? This is the accountant's view of transactions. Every money movement has TWO entries—a debit somewhere and a credit somewhere.
Why do we need it? This is how banks ensure "money never appears or disappears." It's called double-entry bookkeeping.
Banking system diagram 7

Banking system diagram 7

The Golden Rule:
For every debit, there must be an equal credit. The sum of all ledger entries must always be zero.
If it's not zero, money appeared from nowhere or disappeared into nowhere. That's a crime.
sql
CREATE TABLE ledger_entries ( entry_id BIGSERIAL, transaction_id BIGINT NOT NULL, account_id BIGINT NOT NULL, entry_type CHAR(1) NOT NULL, -- 'D' for Debit, 'C' for Credit amount DECIMAL(18,2) NOT NULL, gl_account_id INT NOT NULL, -- General Ledger account created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (entry_id, created_at) ) PARTITION BY RANGE (created_at); -- This query should ALWAYS return 0 -- If it doesn't, something is very wrong SELECT SUM(CASE WHEN entry_type = 'D' THEN amount ELSE -amount END) FROM ledger_entries;
Interview Question: "Why do you need both transactions and ledger_entries tables?"
Your Answer: "The transactions table is customer-facing—it shows what happened to a specific account. The ledger_entries table is accounting-facing—it ensures the bank's books balance. A transfer creates one transaction record but two ledger entries. The ledger is how auditors verify the bank hasn't created or destroyed money."

Table 5: Audit Logs - The Security Camera

What is this table? This records EVERY change to EVERY record in the database.
Why do we need it? When the RBI auditor asks "who changed this customer's address and when?", you need an answer.
Banking system diagram 8

Banking system diagram 8

Key Design Decision: This Table is APPEND-ONLY
sql
CREATE TABLE audit_logs ( audit_id BIGSERIAL PRIMARY KEY, table_name VARCHAR(100) NOT NULL, record_id BIGINT NOT NULL, operation CHAR(1) NOT NULL, -- 'I', 'U', 'D' old_values JSONB, new_values JSONB, changed_fields TEXT[], user_id BIGINT, ip_address INET, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- CRITICAL: No one can modify or delete audit logs REVOKE UPDATE, DELETE ON audit_logs FROM PUBLIC;
Why JSONB for old/new values? Different tables have different columns. Using JSONB lets us store any table's data without creating separate audit tables for each.
json
// Example audit log entry { "audit_id": 12345, "table_name": "customers", "record_id": 67890, "operation": "U", "old_values": {"phone_primary": "9876543210", "email": "old@email.com"}, "new_values": {"phone_primary": "9876543210", "email": "new@email.com"}, "changed_fields": ["email"], "user_id": 100, "ip_address": "192.168.1.1", "created_at": "2024-01-15 14:30:00" }

Chapter 4: The Complete Architecture - How Everything Connects

Now let's see how all these pieces work together.

The Big Picture

Banking system diagram 9

Banking system diagram 9

Let's Walk Through a Money Transfer

Scenario: Rahul wants to transfer ₹5,000 from his account to Priya's account using net banking.
Banking system diagram 10

Banking system diagram 10

Why Each Component Exists

Let me explain why we need each piece of this architecture:

Load Balancer - The Traffic Police

Banking system diagram 11

Banking system diagram 11

Banking system diagram 12

Banking system diagram 12

What it does:
  • Distributes incoming requests across multiple servers
  • If one server dies, routes traffic to healthy ones
  • Can add more servers during peak times (salary day!)
Real-world analogy: Like multiple billing counters at a supermarket. One long queue is slow; multiple counters serve customers faster.

API Gateway - The Security Guard

Banking system diagram 13

Banking system diagram 13

Why Rate Limiting?
Without it, a malicious user could:
  • Send 1 million requests per second
  • Crash your servers (DDoS attack)
  • Try to brute-force passwords
With rate limiting:
  • Each user gets 100 requests per minute
  • Excess requests get rejected with "429 Too Many Requests"

Redis Cache - The Speed Boost

Banking system diagram 14

Banking system diagram 14

Banking system diagram 15

Banking system diagram 15

What we cache:
  • Account balances (read frequently)
  • Customer profile data
  • Recent transactions (for showing history)
  • Session data (login state)
What we DON'T cache:
  • The actual balance during a transfer (must be from database)
  • Anything where consistency is critical
Interview Question: "If you cache the balance, how do you ensure it's accurate?"
Your Answer: "We cache for READ operations like showing balance on screen. For WRITE operations like transfers, we ALWAYS read from the database and use row-level locking. After any write, we invalidate the cache so the next read fetches fresh data."

Kafka - The Message Delivery Guy

This is the most misunderstood component. Let me explain simply:
Banking system diagram 16

Banking system diagram 16

Problem: If SMS service is slow or down, your transfer fails! That's terrible.
Banking system diagram 17

Banking system diagram 17

Why Kafka specifically?
FeatureWhy It Matters for Banking
DurabilityMessages are saved to disk. Even if Kafka restarts, no message is lost
OrderingMessages for the same account are processed in order
ReplayIf fraud detection missed something, you can replay old messages
ScaleCan handle millions of messages per second
Real-world analogy: Kafka is like a postal service. You drop a letter (message) in the mailbox. The postal service guarantees delivery even if the recipient isn't home right now. They'll keep trying until it's delivered.

PostgreSQL - The Source of Truth

Banking system diagram 18

Banking system diagram 18

Why One Primary for Writes?
This is the "single source of truth" pattern. If you have multiple writers:
  • Server A says balance is ₹1000
  • Server B says balance is ₹800
  • Which one is correct?
With single primary:
  • Only PRIMARY knows the real balance
  • All writes go there
  • No confusion, no conflicts
What is Synchronous vs Asynchronous Replication?
SYNCHRONOUS REPLICATION: 1. Write to Primary: "Set balance = 5000" 2. Primary forwards to Sync Replica 3. Sync Replica confirms: "I've saved it" 4. Primary confirms to application: "Done!" If Primary crashes AFTER step 4, Sync Replica has ALL the data. RPO (Recovery Point Objective) = 0 seconds. No data loss.
ASYNCHRONOUS REPLICATION: 1. Write to Primary: "Set balance = 5000" 2. Primary confirms to application: "Done!" 3. Primary forwards to Async Replicas (later) If Primary crashes BEFORE step 3, Async Replicas might be missing recent data. But reads are faster because we're not waiting for confirmation.
Interview Question: "Why not make all replicas synchronous?"
Your Answer: "Synchronous replication adds latency—every write waits for replica confirmation. For high-throughput banking, we use one sync replica (for zero data loss) and async replicas (for fast reads). It's a balance between safety and speed."

Chapter 5: The Transaction Flow - Deep Dive

Let's trace exactly what happens when you transfer money, line by line.

Step 1: The Request Arrives

go
// What arrives at our API type TransferRequest struct { FromAccountID int64 // Sender's account ToAccountID int64 // Receiver's account Amount float64 // How much to transfer Description string // "Payment for dinner" IdempotencyKey string // Prevents double-processing }

Step 2: Idempotency Check

What is Idempotency?
Imagine this scenario:
  1. You click "Transfer ₹5000"
  2. Network is slow, no response
  3. You click again
  4. Now you've transferred ₹10,000!
Idempotency prevents this:
Banking system diagram 19

Banking system diagram 19

go
// Check if we've seen this request before func checkIdempotency(key string) (*TransferResult, bool) { // Look in Redis first (fast) result, err := redis.Get("idempotency:" + key) if err == nil { // We've processed this before! Return cached result return deserialize(result), true } // New request, proceed with processing return nil, false }

Step 3: Lock the Accounts

Why Locking?
Without locking, concurrent transfers can cause chaos:
Time 0: Rahul's balance = ₹10,000 Time 1: Transfer A reads balance = ₹10,000 Time 2: Transfer B reads balance = ₹10,000 Time 3: Transfer A deducts ₹8,000, sets balance = ₹2,000 Time 4: Transfer B deducts ₹8,000, sets balance = ₹2,000 (WRONG!) Result: Rahul transferred ₹16,000 from ₹10,000 account!
With locking:
Time 0: Rahul's balance = ₹10,000 Time 1: Transfer A LOCKS account, reads balance = ₹10,000 Time 2: Transfer B tries to lock... WAIT! Time 3: Transfer A deducts ₹8,000, UNLOCKS Time 4: Transfer B locks, reads balance = ₹2,000 Time 5: Transfer B says "Insufficient balance!" ✓
The Deadlock Problem:
Transfer 1: Lock Account A, then Lock Account B Transfer 2: Lock Account B, then Lock Account A If they run simultaneously: - Transfer 1 locks A, waits for B - Transfer 2 locks B, waits for A - Both wait forever = DEADLOCK!
The Solution: Always Lock in the Same Order
go
func lockAccountsForTransfer(fromID, toID int64) { // ALWAYS lock the lower ID first // This way, everyone follows the same order var firstID, secondID int64 if fromID < toID { firstID, secondID = fromID, toID } else { firstID, secondID = toID, fromID } lockAccount(firstID) lockAccount(secondID) }
Now both transfers will:
  1. Lock Account A first (lower ID)
  2. Lock Account B second
  3. No deadlock!

Step 4: Verify and Execute

go
func executeTransfer(from, to *Account, amount float64) error { // 1. Check balance (inside transaction, with lock held) if from.AvailableBalance < amount { return ErrInsufficientBalance } // 2. Check daily limits todayTotal := getTodayWithdrawals(from.ID) if todayTotal + amount > from.DailyTransferLimit { return ErrDailyLimitExceeded } // 3. Debit sender (with optimistic locking) newFromBalance := from.Balance - amount result := db.Exec(` UPDATE accounts SET balance = $1, version = version + 1 WHERE account_id = $2 AND version = $3 `, newFromBalance, from.ID, from.Version) if result.RowsAffected == 0 { // Someone else modified the account! return ErrConcurrentModification } // 4. Credit receiver newToBalance := to.Balance + amount db.Exec(` UPDATE accounts SET balance = $1, version = version + 1 WHERE account_id = $2 AND version = $3 `, newToBalance, to.ID, to.Version) // 5. Create transaction records createTransactionRecord(from.ID, amount, "DEBIT", newFromBalance) createTransactionRecord(to.ID, amount, "CREDIT", newToBalance) // 6. Create ledger entries (double-entry) createLedgerEntry(from.ID, amount, "DEBIT") createLedgerEntry(to.ID, amount, "CREDIT") return nil }

Step 5: Commit or Rollback

Banking system diagram 20

Banking system diagram 20

The Magic of Transactions:
Everything between BEGIN and COMMIT is atomic:
  • Either ALL changes happen
  • Or NONE of them happen
If the server crashes after debiting but before crediting:
  • The entire transaction is rolled back
  • Sender's money is NOT deducted
  • No money is lost or created

Chapter 6: Scaling - Handling 10 Million Transactions

Read Scaling: Add More Replicas

Most banking operations are reads:
  • Check balance (READ)
  • View transaction history (READ)
  • Transfer money (WRITE)
  • Download statement (READ)
Ratio is typically 80% reads, 20% writes.
Banking system diagram 21

Banking system diagram 21

Adding a replica is easy:
  1. Set up new PostgreSQL server
  2. Point it to primary
  3. It automatically syncs all data
  4. Start routing read queries to it

Write Scaling: This is Harder

Single primary can handle ~10,000 writes per second. What if you need more?
Option 1: Sharding
Split accounts across multiple databases:
Banking system diagram 22

Banking system diagram 22

The Problem: What if Rahul (Shard 1) transfers to Priya (Shard 3)?
The Solution: Saga Pattern
Instead of one transaction across databases, do multiple with compensation:
Banking system diagram 23

Banking system diagram 23

Caching Strategy

Banking system diagram 24

Banking system diagram 24

Cache Invalidation Strategy:
go
// After any account balance change func afterBalanceUpdate(accountID int64) { // Delete from cache redis.Del(fmt.Sprintf("balance:%d", accountID)) // Publish event for other services kafka.Publish("balance_changed", accountID) } // When reading balance func getBalance(accountID int64) float64 { // Try cache first cached := redis.Get(fmt.Sprintf("balance:%d", accountID)) if cached != nil { return cached.(float64) } // Cache miss - get from database balance := db.Query("SELECT balance FROM accounts WHERE account_id = $1", accountID) // Store in cache for next time (expires in 5 minutes) redis.Set(fmt.Sprintf("balance:%d", accountID), balance, 5*time.Minute) return balance }

Chapter 7: Disaster Recovery - When Things Go Wrong

What Can Go Wrong?

Banking system diagram 25

Banking system diagram 25

Multi-Region Setup

Banking system diagram 26

Banking system diagram 26

Backup Strategy

Banking system diagram 27

Banking system diagram 27

WAL (Write-Ahead Log):
Every change to the database is first written to a log file. Even if the server crashes mid-transaction, we can replay the log to recover.
WAL Entry 1: "BEGIN TRANSACTION 12345" WAL Entry 2: "UPDATE accounts SET balance=5000 WHERE id=100" WAL Entry 3: "UPDATE accounts SET balance=15000 WHERE id=200" WAL Entry 4: "INSERT INTO transactions ..." WAL Entry 5: "COMMIT TRANSACTION 12345"
If crash happens after Entry 3:
  • Entries 1-3 are incomplete (no COMMIT)
  • Recovery process ignores them
  • Data returns to pre-transaction state
  • No partial transfer!

Point-in-Time Recovery

Someone accidentally ran DELETE FROM accounts. How do you recover?
bash
# Restore to exactly 2:30 PM yesterday pgbackrest restore \ --stanza=banking \ --type=time \ --target="2024-01-14 14:30:00" \ --target-action=promote
This restores the database to exactly how it was at 2:30 PM, before the bad DELETE.

Chapter 8: Security - Protecting the Money

Defense in Depth

Banking system diagram 28

Banking system diagram 28

Encryption

In Transit: All data moving over network is encrypted with TLS 1.3
Without TLS: "Transfer ₹5000 to account 12345" With TLS: "a7f3b2c9e8d4f1a0b5c6d7e8f9..."
At Rest: All data stored on disk is encrypted
Database file without encryption: "Rahul Kumar, PAN: ABCDE1234F, Balance: 50000" Database file with encryption: "7f3a9c2b8e5d1f4a0c6b9d2e5f8a1b4c..."
Field-Level Encryption: Extra sensitive fields encrypted separately
sql
-- PAN number is encrypted with a different key -- Even DBAs can't see it without special access INSERT INTO customers (name, pan_encrypted) VALUES ('Rahul', encrypt('ABCDE1234F', get_encryption_key()));

Access Control

sql
-- Different roles have different access CREATE ROLE branch_teller; GRANT SELECT ON customers, accounts, transactions TO branch_teller; GRANT INSERT ON transactions TO branch_teller; -- Tellers can view data and create transactions, but can't modify accounts CREATE ROLE branch_manager; GRANT branch_teller TO branch_manager; GRANT UPDATE ON customers TO branch_manager; -- Managers can do everything tellers can, plus update customer info CREATE ROLE fraud_investigator; GRANT SELECT ON ALL TABLES TO fraud_investigator; REVOKE SELECT ON employees FROM fraud_investigator; -- Investigators can see everything except employee data

Row-Level Security

Even with SELECT permission, users only see relevant data:
sql
-- Tellers only see accounts from their branch CREATE POLICY branch_isolation ON accounts USING (branch_id = current_user_branch_id()); -- Now even if teller queries "SELECT * FROM accounts", -- they only see their branch's accounts

Chapter 9: Monitoring - Keeping Watch

What to Monitor

Banking system diagram 29

Banking system diagram 29

Alert Rules

yaml
# Alert if transaction success rate drops below 99.9% - alert: LowTransactionSuccessRate condition: success_rate < 99.9% duration: 2 minutes severity: CRITICAL action: Page on-call engineer immediately # Alert if response time is high - alert: HighLatency condition: p99_latency > 2 seconds duration: 5 minutes severity: WARNING action: Send Slack notification # Alert if replication is lagging - alert: ReplicationLag condition: lag_bytes > 10MB duration: 1 minute severity: CRITICAL action: Page DBA team

Dashboards

Every engineer should have these dashboards:
  1. Business Dashboard: TPS, success rate, volume
  2. Performance Dashboard: Latency percentiles, slow queries
  3. Infrastructure Dashboard: Server health, resource usage
  4. Database Dashboard: Connections, replication, locks

Chapter 10: Interview Cheat Sheet

When Asked "Design a Banking System"

Step 1: Clarify Requirements (2 minutes)
  • "How many users? Transactions per second?"
  • "What types of transactions? Domestic only or international?"
  • "What's the consistency requirement? Can we show slightly stale balance?"
Step 2: Start with Data Model (5 minutes)
  • Draw Customers, Accounts, Transactions tables
  • Explain relationships
  • Mention partitioning for transactions
Step 3: Explain Transaction Flow (5 minutes)
  • Idempotency
  • Locking (and why order matters)
  • Double-entry bookkeeping
  • Commit/Rollback
Step 4: Discuss Scaling (5 minutes)
  • Read replicas for read scaling
  • Sharding for write scaling (mention challenges)
  • Caching strategy
Step 5: Cover Non-Functionals (3 minutes)
  • Disaster recovery (multi-region, backups)
  • Security (encryption, access control)
  • Monitoring (key metrics)

Common Follow-Up Questions

QuestionKey Points to Mention
"How do you prevent double-spending?"Pessimistic locking, version field, serializable transactions
"What if primary database goes down?"Automatic failover to sync replica, < 30 second RTO
"How do you handle peak load?"Read replicas, caching, queue non-critical work
"How do you ensure data consistency?"ACID transactions, double-entry bookkeeping, reconciliation
"What about cross-border transfers?"Separate system, different compliance, Kafka for async

The Gold Standards Checklist

  • ACID compliance for all money movements
  • Double-entry bookkeeping (debits = credits)
  • Optimistic locking (version field)
  • Pessimistic locking (FOR UPDATE)
  • Idempotency for all write operations
  • Partitioned transaction tables
  • Immutable audit logs
  • Field-level encryption for PII
  • Multi-region disaster recovery
  • Comprehensive monitoring and alerting

Summary: What We Built

We designed a banking system that:
  1. Never loses money - ACID transactions, WAL, backups
  2. Never creates money - Double-entry bookkeeping
  3. Never shows wrong numbers - No eventual consistency for balances
  4. Scales to millions - Read replicas, sharding, caching
  5. Survives disasters - Multi-region, automatic failover
  6. Stays secure - Encryption, access control, audit logs
  7. Is observable - Metrics, alerts, dashboards
The key insight: Banking isn't about clever algorithms. It's about being boring, predictable, and reliable. Every design decision prioritizes safety over speed, correctness over convenience.
When you walk into that interview, remember: They're not looking for the fastest system. They're looking for the one that never loses a single rupee.
Good luck! 🏦
All Blogs
Tags:bankingsystem-designdatabaseinterviewarchitectureACIDtransactions