Core banking that balances to the kobo.
General ledger, savings and current accounts, loans, the overnight batch and an audit log, built for microfinance and digital banks in Nigeria. The rules that keep the books right live in the database, so a bug, a retried request or a stray script can’t unbalance them.
| Dr Customer savings 0012 | 25,000.0000 | |
| Cr NIP settlement suspense | 24,975.0000 | |
| Cr Transfer fee income | 25.0000 |
| Dr Customer savings 0044 | 10,000.0000 | |
| Cr NIP settlement suspense | 9,000.0000 |
Example entries. The database checks every entry when it commits. If it doesn’t balance, none of it is saved.
What happens when a posting is made.
This loop shows a transfer being posted, then a faulty entry being refused. It follows the same steps the real system takes.
A customer sends ₦25,000.
Simplified. Figures are examples.
Six services, one ledger.
Each service keeps its own data. Only the ledger’s posting function can write to the books.
General ledger
Double-entry postings against your chart of accounts. A reversal is a new entry that cancels the original. Nothing is edited.
Accounts
Savings and current accounts with versioned products, holds, interest accrual and automatic dormancy.
Loan book
Application to closure: two-person approval, payout, repayment schedules, restructuring, refinancing and write-off.
Overnight batch
Processes every account each night, resumes safely after a crash, and only moves the date on when every account is done.
Audit log
Every change is recorded as an event linked to the one before it by a hash. Editing an old event breaks the chain.
Continuous reconciliation
Balances and sub-ledgers are reconciled on a schedule. Any difference is raised and escalated.
The database enforces the rules.
Many ledgers rely on the application code to get things right. Truss checks again at the database.
- Balanced or refused. A check at commit rejects any entry where debits and credits differ. Nothing is saved.
- Append-only. Journal lines are never updated or deleted. A reversal adds one new entry and marks the original as reversed, in a single step.
- Locked down. The application’s database user can’t write to ledger tables. Only the posting function can, and tests confirm it.
- Exact amounts. Four decimal places, fixed-point, with the currency stored alongside, from the API to disk.
Clear rules for every account status.
Savings and current accounts follow a fixed lifecycle. Each status change is checked by the service, by a database constraint that won’t save an invalid move, and in the same transaction as its history record.
- Versioned products. Changing a rate doesn’t alter the terms an existing customer signed up to.
- Holds and reservations. Funds checks can be safely retried, so a repeated withdrawal request isn’t counted twice.
- Interest and dormancy run as named steps in the overnight batch.
Every loan decision is on the record.
A loan’s status (where it is in its life) is kept separate from its classification (how it’s performing). Reclassifying a loan never changes its status, and each change raises an event finance can use for provisioning.
Prudential classification
Each night the batch works out days at risk from the oldest unpaid instalment and places every active loan in the bank’s configured band. Interest is suspended where the band requires it.
Repayment schedules
Declining-balance annuity schedules with correct month-end dates: a loan taken out on 31 January is due at the end of February. Schedules are versioned, so a restructure keeps the history.
Safe payouts
A loan only goes active once the ledger confirms the payout. If confirmation doesn’t arrive, the loan stays in ‘disbursing’ and the reconciler follows it up.
Every account, every night.
If a step fails on one account, that account is set aside and flagged for the team. The business date only moves on once every account has finished every step, and the system checks this before it advances.
Picks up where it stopped
Each account records the last step it finished, so after a crash the batch carries on without posting anything twice.
Runs in parallel
Accounts are split across workers. If a worker stops, its lease runs out and another worker takes over.
Set up per bank
Steps are named, ordered and switched on for each institution, with a permanent log of every run.
An audit trail you can verify.
Every change to data produces an audit event, and the build fails if any write path is missing one. Events are linked with SHA-256 hashes, so editing an old event breaks every link after it. The latest hash is also stored outside the system and checked regularly.
prev 9f2c…a41ehash 3b7d…c09fprev 3b7d…c09fhash e81a…47b2prev e81a…47b2hash 5c90…d1e3prev 5c90…d1e3hash 0a6f…9b58root 7e44…18acchecked regularlyExample hashes. Reliability uses the same library to link its reconciliation records.
Insert-only
The audit tables accept new events and nothing else. Attempts to update or delete are refused and logged.
Emergency access
Break-glass access uses a separate role that needs approval and leaves a full record.
Privacy requests
Personal data can be erased on a valid request without breaking the hash chain.
Proven tools, used carefully.
Go throughout
Every service is written in Go, with decimal arithmetic for all amounts.
PostgreSQL
One high-availability cluster with a separate database per service. Nothing else holds the official record.
Reliable events
Events go out through a transactional outbox and change data capture, so what’s published matches what was saved.
Kubernetes and GitOps
Deployments are defined in Git and applied automatically. Services talk to each other over mutual TLS.
Separate data per bank
Row-level security on every table, tested on every build, so one institution can’t see another’s data.
Thorough testing
Component tests run against real Postgres. Property-based tests cover every rule. Each release is signed off by an independent tester.
See a posting, a reversal and a night run.
We’ll run them live for your team, check the audit trail, then take your architects through the design.
Book a walkthrough