How money gets recorded, why two records of the same money drift apart, and what I learned fixing it on the Monzo reconciliation team. It ends with one worry about the V3 design.
TL;DR. Every business records money with double-entry bookkeeping: each event hits two accounts, and the books must always balance. Reconciliation is checking your books against an outside truth (the bank) and explaining any difference. Most pain comes from timing and from squashing different things into one bucket. My one worry about V3: posting inbound money to a generic world account throws away the classification finance needs when it later journals into the general ledger.
On the Monzo reconciliation team, we were created after an incident: we held more customer money in our bank accounts than our books explained, and we could not close the financial year until we knew why. This page is the explainer I wish I had then. No accounting background needed.
Accounting in 90 seconds
There is one rule, and everything hangs off it: the books must always balance. That rule is written as the accounting equation.
Assetswhat you own or are owed
=
Liabilitieswhat you owe others
+
Equitythe owners' stake
Assets
What the business owns or is owed. Cash, buildings, money customers owe you.
Liabilities
What the business owes to others. Loans, unpaid bills, customer deposits it holds.
Equity
What is left for the owners once the liabilities are settled. Their stake.
The equation must hold after every transaction. So every event touches at least two accounts: one entry is a debit, the other a credit, and for any transaction the debits equal the credits. That is all "double-entry bookkeeping" means. If the two sides ever disagree, something was recorded wrong, and that gap is what a reconciliation team hunts.
A first transaction: a £100 deposit
A customer pays £100 into their account. Two things happen at once. The bank holds £100 more cash, so an asset goes up. And the bank now owes that £100 back on demand, so a liability goes up.
Customer deposits £100bank's books
Account
Debit
Credit
Cash asset
£100
Customer deposit liability
£100
In plain terms
A customer's money is a liability to the bank, not income. Think of a cloakroom: taking someone's coat does not make you richer by a coat, you simply owe it back. Banks hold customer deposits the same way. Getting this backwards is exactly how you end up holding money you cannot explain.
When do you record it? Cash vs accruals
A second idea decides when an event is recorded, and the same business can look very different depending on the choice. Say you invoice a client £1,000 in March and they pay in May.
Cash accounting
Record money only when it moves. March is silent; the whole £1,000 lands as revenue in May.
May — payment arrivescash basis
Account
Debit
Credit
Cash asset
£1,000
Revenue income
£1,000
Accruals accounting
Record the event when it happens. You earn the revenue in March and only collect the cash in May.
March — invoice issuedaccruals
Account
Debit
Credit
Accounts receivable asset
£1,000
Revenue income
£1,000
May — payment arrivesaccruals
Account
Debit
Credit
Cash asset
£1,000
Accounts receivable asset
£1,000
Accruals records the sale when you shake hands, not when the cash clears.
Under accruals the invoice creates an accounts receivable, an asset that is the money owed to you. When the client pays, you swap that receivable for cash: no new revenue. Most businesses of any size use accruals, and all regulated banks must. It shows the true position, but it is harder to reconcile, because the money and the record of it move at different times. Hold on to that receivable: it comes back at the end.
Reconciliation: two records, one truth
Reconciliation is making sure two independent records of the same money agree. Your ledger says one thing; an outside source of truth (a bank statement, a card-scheme report, a partner's settlement file) says another. Two people counting the same pile of cash, checking they land on the same number. When they agree you are reconciled. When they do not, the difference is a break, and someone has to explain it.
1 · Two sources
Your ledger, and an outside record you trust.
→
2 · Match
Line up entries for the same event, on a shared key.
→
3 · Investigate
Anything in one source but not the other.
→
4 · Explain / fix
Adjust, chase, or fix the bug behind it.
Timing differences (normal)
The money is in flight: one side records it today, the other tomorrow. These clear on their own.
True breaks (dangerous)
A payment recorded twice, a webhook that never fired, a rounding error at scale. These never clear by themselves. They are why the team exists.
How it worked at Monzo
At the center was a ledger service on Cassandra. Any service moving money posted a balanced request to it, tagged with a few fields, and got back a handle to the entries.
From there the data flowed downstream into finance's world once a month.
flowchart LR
L["Ledger service (Cassandra)"] --> BQ["BigQuery raw entries"]
BQ --> DBT["dbt aggregation"]
DBT --> MV["Month-end view of the world"]
MV --> GL["General ledger (NetSuite)"]
Every entry copied into BigQuery, aggregated by dbt, then journaled into the general ledger. Bank and asset accounts came straight from statements instead, an independent truth to reconcile against.
Finance mapped each ledger account to an account in the chart of accounts (for example customer-balance to the Customer balance liability). Once the values were slotted into a spreadsheet, if the leftover difference was not material, the account counted as reconciled. That word "material" does a lot of work: a small difference gets signed off, but a large one you still cannot explain stops everything. The trap below is exactly that, the break that stopped us closing the financial year.
The traps
Timezones: the same day means different things
We took payments through Stripe, which reports and settles on PST. Our ledger ran on UTC. That eight-hour offset means the two sources disagree on where a day starts and ends. A payment near midnight falls on today for us and yesterday for Stripe, so a break appeared every single day. None of it was a real error, but it polluted every daily reconciliation until we aligned the cut-off. Lesson: pin down the timezone of every external feed before you match.
Netting: totals that hide the cause
Cheques clear over five days. Money lands in the bank on day 0 (a debit to the bank asset) but the customer's balance is not credited until day 5. For those days the asset side runs ahead of the liability side. Not by design: we simply had not designed it properly. Nobody fully understood how the customer ledger represented cheques, and how the general ledger accounted for the same cash differently.
Now put every payment rail into one spreadsheet, netted into totals:
Assets
Bank (cheque)£1,700,000
Bank (FPS)£1,300,000
Total£3,000,000
Liabilities
Customer balance£2,500,000
Difference£500,000
There is a £500,000 gap, but why? Because the rails are netted into single totals, you cannot tell if it is uncleared cheques (expected timing), an FPS break (a real error), or something else. The netting destroys the very information you need. You can see that the books do not balance, but not where. Lesson: reconcile per rail, at the grain the money moves, before you net anything.
The fun part
We could not close the financial year. We were sitting on about £1.5M of cash we could not explain, from this bug and a couple of others. That is the incident that created the reconciliation team.
How we fixed it
We reshaped the customer ledger to record money as close as possible to what accounting actually needed. For anything customer-related we tracked the accounting shapes directly: receivable, revenue, deferred revenue, payable, and the actual cash position. The rule was blunt: track everything, and send everything. The ledger always held the raw journals; the gap was that one side of it (the cash) was ignored, and the month-end spreadsheet leaned on the bank statements instead. Now the ledger itself carried the accounting shapes, so finance could journal straight from it.
Each day we emitted a journal straight to the general ledger with the change we expected in each account (for example, +£200,000 at the Bank of England). Then we compared our expected balance against the real balance reported by each external source: the Bank of England statement, the NatWest accounts, the Mastercard settlement files, and so on. If they disagreed, an alert fired and we investigated that day, not at month-end.
The key learning: record the data as close as possible to how finance wants it. That one change simplified everything downstream.
When money comes in, the design posts against a generic world account: the outside counterparty that lets a double-entry movement balance when cash crosses the boundary. As a ledger construct that is normal and fine. The worry is downstream, when we journal into the general ledger, because world collapses different kinds of inflow into one bucket. Money arriving is not one thing.
Settles an invoice
Clears a receivable. The receipt turns money owed into cash.
Client money on account
A liability. Funds the firm holds and owes back to the client.
Not yet attributable
Suspense, until someone classifies which matter or purpose it belongs to.
Written-off invoice, paid late
Bad-debt recovery (income), not a receivable: the receivable was already removed.
Each maps to a different account in the chart of accounts. If the ledger posts them all against world, then world is not one line: it is receivable, liability, and suspense mixed together, and a month-end journal reading the ledger account alone cannot tell them apart. This is the netting trap again, one layer up.
A worked example: an invoice across the period boundary
Assume a firm that closes its books monthly, and an invoice paid directly into the office account. When the invoice and the payment fall in the same month, mapping world to Revenue is correct: it all nets within the period.
The trouble is when they straddle the close. Invoice on the 28th, paid on the 3rd of the next month. Accruals wants the revenue in month 1. So the correct entries are:
Correct (accruals)
Month 1 — invoiceearned
Account
Debit
Credit
Accounts receivable asset
£1,000
Revenue income
£1,000
Month 2 — paymentcollected
Account
Debit
Credit
Office account asset
£1,000
Accounts receivable asset
£1,000
What world does
Month 2 — only when cash movesledger
Account
Debit
Credit
Office account asset
£1,000
world counterparty
£1,000
If world → Revenuewrong
Account
Debit
Credit
Office account asset
£1,000
Revenue income
£1,000
Two things go wrong
The revenue lands in month 2, not month 1 where it was earned. And if the accountant already booked the correct accrual in month 1, revenue is now counted twice. In this straddling case world must instead clear the receivable that month 1 recognized, not re-book revenue.
So the same cash inflow, DR Office / CR world, must map to Revenue in one case and to Accounts receivable in the other. The correct target depends on whether the firm already recognized the revenue in an earlier period, a fact world does not carry. A single static mapping is wrong for one of the two cases.
Either the platform knows which invoice and period the receipt belongs to at posting time, or every firm hand-adjusts accruals each month.
You could try to derive the right account on the fly, from the transaction type, the billing document a receipt was applied to, the account role, and so on. It is possible. It is also error-prone, and it is the brittle multi-source spreadsheet all over again: at Monzo we derived a few schemes that way and paid for it at every month-end. The cheaper lesson from the fix above is the opposite: record the classification at source, in the shape accounting needs, rather than reconstruct it later from world.
Open · needs the money team
This section is pending a discussion with our money team to understand their journaling requirements in more detail. Those requirements should drive what we record, and how, so we record it efficiently. My gut feeling: to journal correctly we will need to track more than just the cash movement from cashiering.
Timezones and legal entities
The Reconciliation PRD sets a cut-off date per period but never says which timezone it uses. That is the Stripe trap, one layer up. At Monzo we started adding a US entity around the time I left, and hit it straight away: each legal entity wants its numbers in its own timezone. A fixed UTC boundary gives the wrong day to at least one of them. Reporting is per day (sometimes per month), so each firm or reporting entity needs a configured timezone, and every daily or monthly cut, whether reconciliation periods, statement matching, or journals, must compute its boundary in that timezone.
Glossary: the words you'll meet
Debit / credit
The two sides of every entry. Not "minus / plus": a debit raises an asset but lowers a liability. What matters is that debits equal credits.
Ledger
The running record of every entry against every account.
General ledger
The firm's official books, where the summarised figures live and financial statements come from.
Chart of accounts
The master list of accounts a business can post to, each typed as asset, liability, equity, income, or expense.
Receivable
Money owed to you that you have earned but not yet collected. An asset.
Suspense
A holding account for money you cannot classify yet, until someone works out where it belongs.
Reconciliation
Proving two independent records of the same money agree, and explaining any difference (a "break").
Materiality
Whether a difference is big enough to matter. Small immaterial differences can be signed off; that judgement can also hide real breaks.