CMD Guide
HomeDatabasesNormalization

Designing a Bank Management System

A bank schema works by making every fact live in exactly one place and then stitching facts back together with foreign keys at query time, so that an account balance, a card linkage, or a loan repayment can never disagree with itself. The hard part of a banking model is not listing entities — it is choosing where money is allowed to be stored versus derived, because a duplicated balance is the most expensive denormalization bug a bank can ship.

We model eight entities — Customer, Account, Transaction, Loan, Loan_Repayment, Branch, Employee, Card — then resolve the one many-to-many relationship (an Account can carry several Cards, a Card can draw on several Accounts) into a junction table. Below, the goal is to reach a schema that is in third normal form (3NF) and to see exactly which anomaly each step removes.

The entities and what each one owns

Each entity owns the attributes that depend on its key and nothing else. Read the table as "this key determines these facts":

EntityPrimary keyOwns (non-key attributes)Points at (FK)
CustomerCustomer_IDFull_Name, City, State, Area, Phone_Number, Email, Date_of_Birth, ID_Type, ID_Number
AccountAccount_IDAccount_Type, Balance, Date_OpenedBranch_ID, Customer_ID
TransactionTransaction_IDType, Amount, Transaction_Date, NotesAccount_ID
LoanLoan_IDLoan_Type, Loan_Amount, Interest_Rate, Start_Date, End_DateAccount_ID
Loan_RepaymentRepayment_IDRepayment_Date, AmountLoan_ID
BranchBranch_IDBranch_Name, Address
EmployeeEmployee_IDName, Role, EmailBranch_ID
CardCard_IDCard_Type, Expiry_Date, Card_LimitCustomer_ID

On Customer.Address: the ER diagram draws Address as a composite attribute (one logical "address" made of City, State, Area). A composite attribute is just a grouping for the diagram — the relational model has no nested columns, so we flatten it into three atomic columns (City, State, Area) in the DDL. That flattening is exactly what 1NF requires: every column holds a single, indivisible value. We do not split it into a separate Address table because each customer has one address that depends fully on Customer_ID — a separate table would buy nothing.

The relationships, and the one that needs a junction table

Seven of the eight relationships are one-to-many, so they need no new table — the "many" side just carries a foreign key. One Customer to many Accounts, one Account to many Transactions, one Branch to many Employees, and so on. The FK lives on the child.

Only Account ↔ Card is many-to-many: a joint current account can have a debit card for each holder, and a single card can be configured to draw on both a checking and a savings account. You cannot put a single FK on either side without losing rows, so this relationship becomes its own table, Account_Card, with a composite key.

diagram
diagram

A traced example: removing a real anomaly

Suppose a junior engineer, wanting fewer joins, denormalizes Account by stuffing the owning customer's details onto every account row:

Account_IDBalanceCustomer_IDCust_NameCust_Phone
A-50112,400.00C-9Asha Rao+91-90000-11111
A-5023,150.00C-9Asha Rao+91-90000-11111
A-77788,000.00C-9Asha Rao+91-90000-22222

Asha changed her phone, the teller updated only the row she happened to open (A-777), and now the bank holds two contradictory phone numbers for one customer. Which one does the fraud-alert SMS go to? This is an update anomaly, and it exists because Cust_Phone depends on Customer_ID, not on Account_ID — a transitive dependency through a non-key column. That is precisely the 3NF violation. Trace the normalization:

  1. 1NF — every column already atomic (no repeating groups, no comma-lists). The composite Address was flattened to City/State/Area for this reason. ✔
  2. 2NF — Account's key is the single column Account_ID, so there are no partial dependencies on "part of" the key. ✔ (2NF only bites when the key is composite, e.g. Account_Card — check that no extra column there depends on only Account_ID or only Card_ID. It has none.)
  3. 3NFAccount_ID → Customer_ID → Cust_Name, Cust_Phone is a transitive dependency. ✘ Fix: drop Cust_Name/Cust_Phone from Account, keep only the FK Customer_ID, and let those facts live once in Customer. Now Asha's phone is stored in a single row; one UPDATE, no contradiction.

The eight CREATE TABLE statements below are already the post-fix, 3NF result — Account holds only Balance, Type, dates, and two FKs.

The schema in SQL

Each entity becomes a table; each one-to-many relationship is a foreign key on the child; the one many-to-many becomes Account_Card. Branch_Phone_Number is split out because a branch can have several phone numbers — a multi-valued attribute, which would violate 1NF if crammed into one column.

CREATE TABLE Customer (
    Customer_ID           INT PRIMARY KEY,
    Full_Name             VARCHAR(100) NOT NULL,
    City                  VARCHAR(50),   -- flattened from composite Address
    State                 VARCHAR(50),
    Area                  VARCHAR(50),
    Phone_Number          VARCHAR(15),
    Email                 VARCHAR(50),
    Date_of_Birth         DATE,
    Identification_Type   VARCHAR(20),   -- e.g. Passport, ID Card
    Identification_Number VARCHAR(50)
);

CREATE TABLE Branch (
    Branch_ID    INT PRIMARY KEY,
    Branch_Name  VARCHAR(50),
    Address      VARCHAR(100)
);

-- Multi-valued attribute → its own table (1NF)
CREATE TABLE Branch_Phone_Number (
    Branch_ID    INT,
    Phone_Number VARCHAR(15),
    PRIMARY KEY (Branch_ID, Phone_Number),
    FOREIGN KEY (Branch_ID) REFERENCES Branch(Branch_ID)
);

CREATE TABLE Account (
    Account_ID   INT PRIMARY KEY,
    Account_Type VARCHAR(20),            -- Savings, Current
    Balance      DECIMAL(15, 2) NOT NULL DEFAULT 0,
    Date_Opened  DATE,
    Branch_ID    INT,
    Customer_ID  INT,
    FOREIGN KEY (Branch_ID)   REFERENCES Branch(Branch_ID),
    FOREIGN KEY (Customer_ID) REFERENCES Customer(Customer_ID)
);

CREATE TABLE Transaction (
    Transaction_ID   INT PRIMARY KEY,
    Transaction_Type VARCHAR(20),        -- Deposit, Withdrawal, Transfer
    Amount           DECIMAL(15, 2) NOT NULL CHECK (Amount > 0),
    Transaction_Date TIMESTAMP NOT NULL,
    Notes            TEXT,
    Account_ID       INT,
    FOREIGN KEY (Account_ID) REFERENCES Account(Account_ID)
);

CREATE TABLE Loan (
    Loan_ID       INT PRIMARY KEY,
    Loan_Type     VARCHAR(20),           -- Home, Personal, Car
    Loan_Amount   DECIMAL(15, 2),
    Interest_Rate DECIMAL(5, 2),
    Start_Date    DATE,
    End_Date      DATE,
    Account_ID    INT,
    FOREIGN KEY (Account_ID) REFERENCES Account(Account_ID)
);

CREATE TABLE Loan_Repayment (
    Repayment_ID   INT PRIMARY KEY,
    Repayment_Date DATE,
    Amount         DECIMAL(15, 2),
    Loan_ID        INT,
    FOREIGN KEY (Loan_ID) REFERENCES Loan(Loan_ID)
);

CREATE TABLE Employee (
    Employee_ID INT PRIMARY KEY,
    Name        VARCHAR(100),
    Role        VARCHAR(50),
    Email       VARCHAR(50),
    Branch_ID   INT,
    FOREIGN KEY (Branch_ID) REFERENCES Branch(Branch_ID)
);

CREATE TABLE Card (
    Card_ID     INT PRIMARY KEY,
    Card_Type   VARCHAR(20),             -- Credit, Debit
    Expiry_Date DATE,
    Card_Limit  DECIMAL(15, 2),
    Customer_ID INT,
    FOREIGN KEY (Customer_ID) REFERENCES Customer(Customer_ID)
);

-- Junction table resolving the Account ↔ Card many-to-many
CREATE TABLE Account_Card (
    Account_ID INT,
    Card_ID    INT,
    PRIMARY KEY (Account_ID, Card_ID),
    FOREIGN KEY (Account_ID) REFERENCES Account(Account_ID),
    FOREIGN KEY (Card_ID)    REFERENCES Card(Card_ID)
);

Why the naive Transaction model is wrong

The textbook Transaction above has exactly one Account_ID. That correctly records a deposit or withdrawal, but it cannot honestly represent a transfer, which is two-sided: money must leave one account and arrive in another, atomically. With one FK you are forced into one of two broken shortcuts:

The honest fix is double-entry: keep Transaction as the event ("Transfer, ₹5,000, 14:32") and add a Transaction_Leg table with one row per affected account and a signed amount, where every transaction's legs must sum to zero. Then a transfer is one Transaction with two legs (−5,000 on A-501, +5,000 on A-777), wrapped in a SQL transaction so it commits all-or-nothing. This is how real ledgers are built, and it is why banks never store Balance as the single source of truth — balance is the running sum of legs, and the stored column (if kept at all) is a cache reconciled against the ledger.

diagram
diagram

Pitfalls

Takeaways


Synthesized from Elmasri & Navathe, Fundamentals of Database Systems (composite vs. multi-valued attributes, ER-to-relational mapping, normal forms); Silberschatz, Korth & Sudarshan, Database System Concepts (functional dependencies and 3NF); and the double-entry ledger model documented in Martin Kleppmann, Designing Data-Intensive Applications and in payment-ledger engineering write-ups (e.g. Square/Stripe and the TigerBeetle ledger design notes). Re-authored and deepened for this guide — fixed the copy-pasted "HMS"/"Hospital Management System" references, explained the Address composite-to-atomic flattening, added an explicit 1NF→2NF→3NF trace with a worked update anomaly, and added the double-entry correction to the naive single-account Transaction model.

🤖 Don't fully get this? Learn it with Claude

Stuck on Designing a Bank Management System? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Designing a Bank Management System** (Databases) and want to truly understand it. Explain Designing a Bank Management System from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Designing a Bank Management System** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Designing a Bank Management System** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Designing a Bank Management System** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes