Designing a chart of accounts you can reconcile
What finance people mean by a ledger, and the modelling mistakes that make month-end hurt.

Published 2026-10-15 by Arif Hossen
Tags: Schema
Source: https://arifhossen.dev/blog/designing-a-chart-of-accounts-you-can-reconcile
Where it started
The finance team at an East African Islamic insurer started using the statements I had built in earnest, and they did not balance. The reports summed only the lowest-level accounts. They ignored balances carried forward from prior years. There was no retained-earnings line. And in this kind of insurer every one of those figures has to appear twice, once for the participants' pool and once for the shareholders' fund.
A statement that does not balance is not a cosmetic bug for a finance head. It is a stop. Every one of those mistakes was in the model, not in the arithmetic.
One ledger, two sets of books
Two funds
An Islamic insurer keeps the participants' money and the shareholders' money apart. Not as a report filter, but as two sets of books that each have to balance on their own.
So the fund is a property of the account, not of the posting. Every account belongs to exactly one fund. The same name can exist in both, each with its own code. Retained earnings exist once per fund. Business modules never name an account directly. They ask a mapping for salary expense or retained earnings in a given fund, so payroll and claims post to the right book without hard-coding a code.
Let the code carry the hierarchy
Codes
Account codes are generated, never typed. Seven digits, where the trailing zeros give the level.
1000000 assets (category root)
1100000 group
1110000 sub-group
1110001 postable leaf
Trailing zeros give the level
Two rules follow from that, and both are enforced. A parent account cannot take postings, only leaves can. And an account that already has journal lines cannot be turned back into a parent: the save is refused with a reason.
Those rules are what make the roll-up possible. If a parent can hold its own postings, its balance is no longer the sum of its children, and nobody can tell which number is right.
Make the wrong entry impossible to post
The ledger refuses
The ledger accepts a journal only when it is approved, has a fund, and balances. An unbalanced entry is refused with a message naming the debit total, the credit total and the difference.
Once posted, an entry is read-only. A mistake is corrected by a reversal: a mirror entry with a written reason, linked to the original, which stays visible. Nothing is edited after the fact.
Time is controlled the same way. Each month is a period that closes itself after a grace window, locks after two months, and can be reopened only one step back, with approval. Journals, payments, purchase orders and payroll all check the period before they post.
Recoding a chart that already has history
Changing it live
Seven months in, finance needed seven-digit codes instead of six. By then the chart held real accounts with real postings.
I shipped it as one reviewed change with three reversible conversions: one to convert the codes, one to fix parent codes the old format had left wrong, and one to fix leaves that pointed at the wrong sub-parent. The seeders moved to the new format in the same change, along with a seeder that loads the client's real chart, so test environments rehearsed against the real thing.
The statements were rebuilt, not patched
What it cost
The fix for the imbalance went into the calculation layer. Parent accounts roll up their children, prior years carry forward, retained earnings sit per fund, and a year-end close moves the result into them. Posted transactions were not touched. Over 1,100 lines of tests prove each parent equals the sum beneath it.
The lesson I took: roll-ups, carry-forwards and retained earnings are not reporting features to add later. They are the model. I built journals and a ledger first and statements on top, and finance found the gap before I did.