Browse Templates/Banking System

Banking System

A comprehensive core banking database schema for retail and digital banking platforms. Includes customer onboarding with KYC, multi-type account management (checking, savings, business), transaction ledger with double-entry support, inter-account transfers, debit/credit card issuance, and loan lifecycle tracking with interest rates. Suitable for neobanks, fintech startups, or traditional bank modernization projects.

PostgreSQL10 tablesFinance
financebankingaccountstransactionsloanscards
Created with ERD Studio

About this schema

transactions.balance_after is the column that makes this a banking schema rather than a list of amounts. Every row records what the balance became, so a statement can be printed without replaying the account from opening, and any disagreement between the running figure and accounts.balance is detectable at the exact row where it started.

A transfer is one row in transfers and two in transactions. The transfer is the instruction with its own status; the transactions are what actually happened to each account. That separation is what lets a transfer be pending, fail, or reverse without leaving one side debited and the other untouched.

Cards never store a card number. cards keeps a card_number_hash for matching and last_four for display, which is the only arrangement where a leaked database does not hand over usable card credentials — the real number lives with the issuer and this row refers to it.

Key relationships

customers → accounts → transactions
The person, the account, and its ledger. balance on the account is the cached total that transactions prove.
account_types → accounts
Interest rate and minimum_balance are product terms, so changing a product does not require touching every account that uses it.
transfers → accounts (from, to)
Two keys into the same table on one row, plus a status — the instruction, distinct from the two ledger rows it produces.
accounts → cards
A card is an access method for an account, which is why daily_limit sits here rather than on the account itself.
loans → loan_payments
The schedule is rows, each splitting principal_amount from interest_amount, so an amortisation table is data rather than a formula.
customers → beneficiaries
Saved recipients are external account numbers, not foreign keys, because the receiving bank is usually not this one.

Design decisions

The balance is both stored and derivable

accounts.balance is what every screen reads, and the transactions ledger is what proves it. Keeping only the ledger makes every balance check a sum over an unbounded table; keeping only the column leaves no way to explain a figure. Both means a transaction must update the account in the same atomic write, and balance_after makes the two reconcilable row by row rather than only in aggregate.

Transfers are an instruction, not a transaction type

Recording a transfer as two transactions alone loses the fact that they belong together — a failed second leg would look like a withdrawal that simply vanished. The transfers row carries the status of the whole operation, which is what makes a reversal possible and what a customer sees when the money has left but not yet arrived.

Loan payments are written in advance

The full schedule exists as rows with due dates before any money moves, each splitting principal from interest. That makes the next payment a query rather than a calculation, lets a missed payment be a row with a status instead of an absence, and keeps the amortisation visible to the customer. Early repayment means rewriting the remaining rows, which is the known cost.

kyc_status is on the customer

Identity verification is a property of the person, not of each account they hold, and regulation requires it before an account can transact. Keeping it on customers means one check gates everything they own, and a customer whose verification lapses is blocked everywhere at once rather than account by account.

Tables in this template

customersCustomers
9 cols
account_typesAccount Types
7 cols
accountsAccounts
9 cols
transactionsTransactions
8 cols
transfersTransfers
7 cols
cardsCards
9 cols
loansLoans
9 cols
loan_paymentsLoan Payments
8 cols
beneficiariesBeneficiaries
8 cols
notificationsNotifications
7 cols

Frequently asked

Why store balance_after on every transaction?
So a statement is readable without recomputing from the beginning, and so a corrupted balance can be traced to the exact transaction where the ledger and the account stopped agreeing.
How is a transfer kept consistent?
Both transactions and the account balance updates happen in one database transaction, with the transfers row recording the outcome. Without atomicity the debit can land while the credit does not, which is the failure every banking schema is designed to prevent.
Should balances be integers?
They must not be floating point. Use a decimal or an integer in minor units — a floating-point cent of error is a reconciliation failure, and in a banking ledger it compounds across every subsequent balance_after.
Why does transactions have a reference_id?
To point back at what caused the entry — a transfer, a loan payment, a card authorisation. It is an untyped reference, so the application is responsible for knowing which table it names.
What is missing for a real core banking system?
Holds and pending authorisations, interest accrual as its own postings, and an immutable audit of who performed each operation. This schema shows the account and ledger structure; it does not model the controls a regulated institution is required to run.