DEV Community

Cover image for Designing a chart of accounts you can reconcile
Arif Hossen
Arif Hossen

Posted on Originally published at arifhossen.dev

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
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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
arhancanli profile image
Arhan Canli •

The trailing-zeros trick has one edge worth testing: with the layout in your example (1 / 1 / 1 / 4-digit leaf), the leaf 1110010 also ends in a zero, so if anything infers level from the code alone it will read as a deeper parent. The 1110001 example only works while the leaf suffix never ends in 0, so I'd keep level as a stored column and treat the code as a label, or start leaf numbering so the last digit is never 0. A cheap invariant test: for every account, the set of leaves under it computed by code prefix must equal the set computed by parent_id.

The reversal rule raises a period question: once a month has locked after two months, which period does the mirror entry of an old mistake post into, the original's (needs a reopen) or the current one? Both are defensible, but the choice changes which month's retained earnings move, per fund. Which did finance pick?