A banking system: money is moved, never just changed

Published . By the DiagramDesk team.

The first design most people draw for a bank has an Account with a balance, and a transfer that subtracts from one balance and adds to another. It works until anyone asks what happened: where the 250 went on Tuesday, or why two numbers disagree. A bank's model is built round the record of movements, and the balance comes second.

The ledgerA transferClassesUse casesData flowMistakes

Accounts, holders and a ledger of entries

Three decisions shape the ER model. The first is that an account can have more than one holder. A joint account belongs to two customers, and one customer often holds several accounts, so the two are many-to-many, resolved by Account holder with a role saying whether this customer is a sole or joint holder. Its key is the customer and the account together, so both of its lines are solid, identifying relationships. So is the line to Loan, which is keyed by the account it belongs to; the other lines are dashed, because Account and Entry each have a key of their own.

The second is the ledger. Every movement of money is a Transfer made of Entries, each of which adds to or takes from one account. Moving 250 from savings to current is one transfer with two entries, minus 250 on savings and plus 250 on current, and they sum to zero. That is double-entry bookkeeping reduced to its core, and it is what lets anyone reconstruct any balance on any day by adding up the entries.

A transfer is at least two entries; an account's history is its entries.is named inis held bykeepsis moved byis made ofcarriesCUSTOMERint customer_id PKstring namedate date_of_birthACCOUNT_HOLDERint customer_id PK, FKstring account_no PK, FKstring role "sole or joint"ACCOUNTstring account_no PKstring branch_code FKstring kinddecimal balance "kept in step with entries"BRANCHstring branch_code PKstring nameENTRYint entry_id PKint transfer_id FKstring account_no FKdecimal amount "negative out, positive in"TRANSFERint transfer_id PKdatetime made_atstring referenceLOANstring account_no PK, FKdecimal principaldecimal annual_ratedate ends_on
A transfer is at least two entries; an account's history is its entries.Open in the editor

The third decision is what to do with the balance. It could be left out entirely and worked out from the entries every time, which is correct but slow for an account with years of history. So the model keeps it, with a note that it is kept in step with the entries: it is a stored copy of a sum, updated in the same unit of work as the entries that change it, and if the two ever disagree the entries win. Saying that in one sentence in a report is worth more than either choice made silently.

Account numbers are stored as text, not as numbers. Nobody adds two account numbers together, and a number type would drop a leading zero.

A transfer, all or nothing

The sequence diagram for a transfer between two of the customer's own accounts shows the rule that matters most in banking software: both entries are written, or neither is. The transfer service opens one unit of work (a database transaction), locks both accounts so no other transfer can change them halfway, checks the money is there, writes the two entries and commits. If anything fails, it rolls back, and the ledger looks as if nothing was attempted.

Begin, lock, check, write both entries, commit; or roll back.alt [Enough money]Move 250 from savings to currentTransfer 250, savings to currentBegin one unit of workLock both accountsSavings holds 900Entry: savings, minus 250Entry: current, plus 250CommitCommittedDone, reference T-4471TransferredRoll backRefused, insufficient fundsNot transferredCustomerBanking appTransfer serviceLedger[Not enough]
Begin, lock, check, write both entries, commit; or roll back.Open in the editor

The lock is what a first attempt leaves out, and it is easy to justify in a write-up. Without it, two transfers from the same savings account at the same moment could each read 900, each decide there is enough, and together take out more than was there. The alt frame's second half is not an error screen afterthought: a refused transfer is a normal outcome and belongs in the diagram.

Classes: kinds of account

Savings and current accounts differ in one rule, whether the account can pay out a given amount. A savings account cannot go below zero; a current account can, down to its overdraft limit. That is the classic case for an abstract method: Account declares canPay(amount), and each subclass answers it its own way.

Transfer owns its entries and can check that they sum to zero.1..* · holds · 0..*1 · is made of · 2..*0..* · moves · 1«abstract» Account+accountNo: String+balance() Money+canPay(amount) bool*SavingsAccount+annualRate: Decimal+canPay(amount) bool+addInterest(month)CurrentAccount+overdraftLimit: Money+canPay(amount) boolCustomer+name: String+accounts() List<Account>Transfer+reference: String+madeAt: DateTime+isBalanced() boolEntry+amount: Money
Transfer owns its entries and can check that they sum to zero.Open in the editor

Transfer.isBalanced() is the class-diagram form of the double-entry rule, a check that the entries sum to zero before anything is saved. The multiplicity on Customer to Account is 1..* at the customer end: every account has at least one holder, and a joint account has more.

Use cases

A customer moves money between their own accounts, pays people at other banks and reads statements. A teller opens accounts and handles cash over the counter. A loan officer approves loans. Other banks appear as one secondary actor, because a payment to someone else's bank leaves this system and has to be sent through the channels banks use between themselves.

Opening an account always includes checking identity documents.Core banking system«include»«include»Transfer betweenown accountsPay someone atanother bankView a statementOpen an accountPay in or take outcashApprove a loanSend an interbankpaymentCheck identitydocumentsCustomerTellerLoan officerOther banks
Opening an account always includes checking identity documents.Open in the editor

Transfer between own accounts and Pay someone at another bank are separate use cases even though both move money, because only one of them involves another bank and the steps differ. Checking identity documents is «include»d in opening an account because a bank cannot skip it, which is also why it is drawn as its own use case: it appears in the requirements as a named step.

Data flow: entries in, statements out

The level 1 DFD has one feature worth pointing out to a marker. Process 2.0, adding monthly interest, has no outside entity feeding it: it is started by the calendar and reads rates and balances from D1. That is allowed. A process needs data in and data out, and a store can be its only source.

Interest is posted as entries too, so statements need nothing but D2.transfer instructionbalances, limitspaired entriesnew balancesoutgoing paymentreceiptrates, balancesinterest entriesnew balancesmonth's entriesstatementCustomer1.0Move money2.0Add monthly interest3.0Produce statementOther banksD1 AccountsD2 Entries
Interest is posted as entries too, so statements need nothing but D2.Open in the editor

Interest is written as entries, like every other movement, which is why the statement process reads D2 and nothing else. Like a transfer, process 2.0 also writes the new balances back to D1, because the model keeps each stored balance in step with its entries. A statement is a list of entries for one account over one month, and a model in which interest changed the balance directly would have a gap in every statement.

The mistakes to avoid

  1. A balance with no history. Without entries there is no statement and no way to explain a number.
  2. A transfer that updates two balances without one unit of work, so a failure halfway loses money.
  3. One customer per account, which rules out the joint account every bank offers.
  4. A single Transaction row with "from" and "to" columns, which cannot describe a movement with three parts, such as a payment with a fee.
  5. An account type field and switch statements, instead of subclasses answering canPay.
  6. Account numbers stored as numbers.

If your brief is for a cash machine rather than the bank behind it, the ATM example covers the machine's side of the same conversation.