- engineering
- ledger
- trust
Why a Double-Entry Ledger Matters for African Fintech
By Sente Team · 21 October 2026 · 6 min read
At Sente, every money movement — a USSD airtime top-up in Nairobi, a merchant checkout in Lagos, a transfer from a market trader in Harare to her supplier across the border — is recorded as a set of double-entry journal postings whose debits and credits must always net to zero.
That sentence sounds like something from a first-year accounting textbook. It is. Double-entry bookkeeping has been the backbone of commerce since Venetian merchants wrote it down in the fifteenth century. What is surprising is how many modern payment systems quietly abandon it, and how much trouble that causes.
This article explains what a double-entry ledger is, why we built Sente around one, and what it changes for the people and businesses whose money moves through the platform.
The shortcut most systems take
The simplest way to build a wallet is to give every account a balance column and change it whenever money moves:
UPDATE accounts SET balance = balance - 500 WHERE id = 'amina';
UPDATE accounts SET balance = balance + 500 WHERE id = 'kito';It is fast to write, easy to read, and it works — right up until it doesn't.
Imagine the first statement succeeds and the process crashes before the second one runs. Five hundred shillings have left Amina's account and arrived nowhere. Or imagine two requests try to spend the same money at the same moment, each reading the balance before the other has written it. Or imagine a bug in a fee calculation that subtracts a little too much, a few thousand times a day, for a month.
In every one of those cases the system now holds a number it believes to be true, and nothing that can prove it wrong. The balance column is the record. If it is corrupted, the truth is gone.
What double entry actually means
A double-entry ledger never stores "the balance" as the source of truth. It stores postings: immutable lines that say a specific amount moved into or out of a specific account, as part of a specific journal entry.
Every journal entry must balance. When Amina sends Kito 500 shillings and Sente charges a 5-shilling fee, the entry looks like this:
| Account | Debit | Credit |
|---|---|---|
| Amina's wallet | 505 | |
| Kito's wallet | 500 | |
| Sente fee income | 5 |
The debits (505) equal the credits (500 + 5). The sum of debits minus the sum of credits is zero. That is the invariant, and it holds for every single entry the platform has ever written:
Σ debits − Σ credits = 0
Balances are then derived: Amina's balance is the sum of everything that ever moved into her account minus everything that ever moved out. We keep snapshots so that reading a balance is fast, but those snapshots are append-only, versioned, and can always be rebuilt from the postings underneath them.
Why it matters
Money can't appear or vanish
Because every entry must balance, money cannot be created or destroyed by accident. A bug that credits Kito without debiting someone else simply cannot be written: the entry would not net to zero, and the ledger rejects it. The half-finished transfer from the example above is impossible too, because the whole entry is written in one transaction or not at all.
Every figure can be explained
When a customer calls support and asks why their balance is what it is, the answer is not "the database says so". It is a list of postings, each tied to a payment, a fee or a reversal, each with a timestamp and a reference. The same is true for a merchant reconciling a day of sales, an auditor checking our books, or a regulator asking how a specific amount moved.
Mistakes are corrected in the open
Postings are never edited or deleted. If something was wrong — a fee charged twice, a payment sent to the wrong account and later recovered — we correct it with a new, balancing entry that reverses the effect, and both the mistake and the correction stay on the record. That is uncomfortable for engineers who like a tidy database, and exactly what you want when the database holds other people's money.
A damaged balance can be rebuilt
If a snapshot row is ever lost or corrupted — a disk failure, a bad deploy, a bug in the snapshot logic — it can be recomputed from the postings. We test this continuously: our reconstruction tests rebuild balances from scratch and compare them with the snapshots, and any difference is treated as an incident.
How we built it
A few rules, enforced in code and in review, make the ledger trustworthy in practice and not just in principle.
Integer minor units, always
Money at Sente is never a floating-point number. Every amount is an integer count of the currency's smallest unit — cents, kobo, thebe — paired with an ISO 4217 currency code. Floating-point arithmetic cannot represent most decimal fractions exactly, and the tiny rounding errors it produces add up, quietly and permanently, across millions of transactions. Integers do not drift.
The same rule stops a quieter bug: adding Kenyan shillings to Nigerian naira. Cross-currency arithmetic throws an error instead of producing a meaningless number. Converting between currencies is always an explicit step with a quoted, locked rate.
Serializable transactions
Every write that touches money runs at PostgreSQL's strictest isolation level, SERIALIZABLE. The database guarantees that concurrent transactions behave as if they had run one after another. Two requests racing to spend the same balance cannot both succeed; one of them is told to retry.
Retries are bounded, with exponential backoff and jitter, so a burst of contention does not turn into a storm. If a transaction still cannot complete, it fails cleanly and the caller is told — nothing is half-written.
One ledger for every channel
The app, USSD, agents, merchant checkout and the developer API all post to the same ledger. There is no separate "USSD balance" that has to be synchronised with an "app balance" at night. A payment made by dialling a short code on a feature phone is visible in the app the moment it posts, because there is only one record.
Holds for money in flight
Some payments depend on someone else: a mobile-money operator, a bank, a card scheme. We never call those providers inside a ledger transaction. Instead we place a hold — a posting that reserves the funds — then call the provider, then either post the payment for real on success or release the hold on failure. At every moment the ledger knows exactly how much money is committed, available or on its way.
What it changes for you
All of this is invisible when it works, which is the point. But it shows up in ways that matter:
- For people, it means a balance you can trust and a history that always adds up, on any phone.
- For merchants, it means a day's takings reconcile to the shilling, across every way customers paid.
- For partners and regulators, it means every movement can be traced end to end, with nothing edited after the fact.
- For developers building on Sente, it means the money your product moves is accounted for with the same rigour as a bank's books, without you having to build it.
Trust is the product
In markets where many people have been burned by a failed savings scheme, a mobile wallet that froze, or a transfer that never arrived, trust in digital money is earned slowly and lost quickly. A double-entry ledger doesn't make headlines. It is what lets us answer, for any payment, the only question that really matters: where did the money go, and can you prove it?
We can. That is why we built it this way.
Building on Sente? The developer guide shows how payments you create through the API land on the same ledger.