Four tables and one join table carry most of retail banking, and the join table
is the one that gets flattened away.
party a person or legal entity. Diligence attaches here.
party_id, party_type, legal_name, date_of_birth, country,
risk_rating, kyc_case_id
account_party the relationship. Many to many, and effective dated.
account_id, party_id,
role holder | signatory | beneficial_owner | mandate_delegate
valid_from, valid_to
account the container postings are made against.
account_id, product_version_id, currency, status,
opened_at, closed_at -- closed, never deleted
product_version the terms. New row per change, never edited in place.
product_version_id, product_id, version, interest_rate,
fee_schedule_id, effective_from, effective_to
posting the ledger. Append only. One row per side of a transaction.
posting_id, account_id, dr_cr, amount_minor, currency,
transaction_id, value_date, booked_at
The role column is the whole point of the join table. A signatory can instruct
payments but is not an owner of the funds, a beneficial owner is subject to
diligence but cannot instruct anything, and a delegate holds a time-boxed mandate
that must expire. Systems that store a single customer_id on the account cannot
express any of that, and the workaround is invariably a second hidden account per
extra party, which then breaks every balance and every statement.
Effective dating on account_party is what lets you answer who was authorised on
the day a disputed payment was made. Without valid_from and valid_to you know
the current mandate and nothing else, and the dispute you are being asked about is
eight months old.
currency sits on the account and not on the product because a
multi-currency product issues one account per currency, and a posting must never
mix units. Storing amounts as integer minor units rather than a decimal or a
float is the other non-negotiable, since binary floating point cannot represent a
tenth of a penny and a rounding error in a ledger is a reconciliation break
somebody has to investigate.
The closed_at column instead of a delete is the pattern repeated throughout
banking. Nothing is removed, because the transaction history, the diligence
evidence and the tax records all outlive the relationship, and an erasure request
against a closed account is answered by a legal retention exemption rather than a
DELETE.