Selected engineering
01The money path

A double-entry posting engine that cannot silently go wrong

Seventy-four places in the system move money. All seventy-four go through one piece of code, and nothing reaches the accounts any other way.

Sole engineer — design, build, production support20+ live tenants · 34 live transaction types.NET 8 · EF Core 8 · MySQL 8In production
General Ledger / Journal Entries / JE-2026-0412Balanced
Journal entry — Sale
AccountDebitCredit
1200Accounts Receivable1,240.500—
4000Sales Revenue—1,181.429
2300VAT Output Tax—59.071
5000Cost of Goods Sold742.000—
1300Inventory—742.000
Totals1,982.5001,982.500
ΣDr = ΣCrTx type · Saledecimal(18,3)→ INV-88213

Interface rendered in code from the platform’s own structure — illustrative, not a screenshot. Figures are illustrative and balance as the engine requires.

The problem

A sale, a purchase, a return, a damaged item written off, a till that is short — every one of them moves money, and every one has to land on the right accounts. Get it wrong and the books are quietly incorrect: no error, no alert, and nobody finds out until a year-end that does not reconcile. Get it defensive and you block the sale, which stops a shop trading.

A multi-tenant ERP and POS platform running the daily operations of twenty-plus retail businesses across Kuwait and the Gulf, including a mid-size superstore. Real cash, real inventory, real tax filings — and no second engineer to catch a mistake in review.

What I built

All of it. I designed the engine, chose its four-stage shape, wrote the rules format, built it, deployed it and support it in production. There was no architect to hand me a spec and no reviewer to disagree with me.

I made the engine four stages, each replaceable. A classifier turns a business event into a transaction type. A rules provider supplies declarative posting rules for that type, with reversal symmetry built into the format rather than written twice. An account resolver maps those rules onto the chart of accounts through 67 named posting roles, so a rule says what it is posting and never which account number. A journal builder writes the balanced debit and credit lines, and each document keeps the tax rate it was posted at. The rules are data rather than code because an accountant can read data, compare two versions of it and argue with it. Seventy-four hand-written posting routines would give them nothing to read.

POSTING ENGINE — CENTRALISED, DECLARATIVE1Classifierevent → transaction typesale · return · COGS · cash variance2Rules providerdeclarative posting rulesreversal symmetry built in3Account resolver84 categories · 67 posting rolesfalls back to Suspense4Journal builderbalanced debit / credit linesper-document tax snapshotGUARDΣDr ≠ ΣCr → the posting throws · wrapped in a DB transaction · decimal(18,3) · optimistic concurrency · gap-free document numberingBALANCED ENTRYGeneral Ledgerappend-only entries and lines · reversal handling · audited post-posting edit with before-image and delta journals · fiscal-period handlingSINGLE SOURCETrial Balanceevery account, any dateBalance Sheetretained earnings split✓ assets =liabilities + equityProfit & Lossperiod revenue vs cost✓ net profit ties toequityGL drill-downaccount → entry → documentSUBLEDGERS — AR / AP WITH FIFO SETTLEMENT · POINT-IN-TIME AGING · RECONCILED TO GL CONTROL ACCOUNTS
Account resolution never guesses and never blocks a sale. If the first three tiers miss, the entry posts to Suspense — balanced, visible, and queued for a human.

The decision with no clean answer

When the resolver cannot find an account, there are three options and all three are bad. Guess an account and the books are corrupted silently — the worst possible outcome, because it is invisible. Block the sale and a shop with a customer at the till stops trading over a configuration gap. Post to Suspense and the entry is balanced, visible, wrong in a way anyone can see, and queued for a human. I chose Suspense. It is the only one of the three that is honest and still keeps the till open.

Proof — one real sale

The panel at the top of this page is drawn in code. This one is not. These are the six journal lines the engine wrote for one real sale, and four things are visible in them without reading a line of source.

General Ledger / Journal Entries / SALE-1Balanced
Journal entries — one cash sale
AccountDebitCredit
1120Accounts Receivable · SALE-1 · Sale receivable0.875—
4100Sales Revenue – Products · SALE-1 · Sales revenue – products—0.875
5101COGS – Goods Sold · COGS-1 · Cost of goods sold0.750—
1130Inventory · COGS-1 · Inventory reduction—0.750
1115Cash Register 1 · PAY-1-1 · Customer receipt0.875—
1120Accounts Receivable · PAY-1-1 · AR settlement—0.875
Trial balance, period2.5002.500
ΣDr = ΣCrTx types · Sale · COGS · Receiptdecimal(18,3)

Read off the running system on 13 September 2026: one item, paid in cash, on a demo company. Unedited.

  • The cash sale goes through Accounts Receivable and is then settled, so a cash sale and an account sale share one code path.
  • Cost of goods posts at moving average cost: 0.750 against a 0.875 sale, tying exactly to the 0.125 profit the sales history reports.
  • The cash landed on Cash Register 1, that till's own account — the override tier of the account ladder, working.
  • No service-revenue line appears, because the basket had no service item. Empty lines are dropped, so opening a till with a zero float posts nothing at all.

What it produced

  • Nothing persists unbalanced — if debits and credits disagree the posting throws inside a database transaction, so a half-written journal cannot exist.
  • Money is decimal(18,3) end to end, full precision in, rounded once at the edge.
  • Document numbering is gap-free, which is a legal requirement in most of the jurisdictions this runs in.
  • Every ledger line links back to the document that created it — you can go from a figure on a customer's screen to the journal lines behind it, and back to the invoice.

What I would do next

Automated tests on the financial code. There are none on the web platform, and for the parts I refactor slowly that means nothing tells me when I have broken something. It is the single thing I would change about this engine.

Appendix — all 34 transaction types

34 declarative types. Each one states what it debits and credits.

  • Sale
  • SalesReturn
  • SalesReturnDamaged
  • SaleAdjustment
  • SaleDiscountChannelAdjustment
  • CustomerDiscount
  • PromotionalGiveaway
  • Receipt
  • COGS
  • Purchase
  • PurchaseReturn
  • PurchaseInvoicePost
  • PurchaseInvoiceVoid
  • SupplierPayment
  • SupplierRefundPayment
  • SupplierWriteOff
  • WriteOff
  • InventoryAdjustment
  • InventoryAdjustmentVoid
  • StockInTransit
  • StockInTransitReceive
  • StockInTransitWriteOff
  • StockReceive
  • StockReceiveVoid
  • OperatingExpense
  • CashShortageSurplus
  • CashDeposit
  • CashTransaction
  • SafeDrop
  • RegisterOpen
  • RegisterClose
  • CommissionAccrual
  • CommissionPayment
  • CommissionReversal
  • Reversal counterpart

Ten minutes on a screen-share tells you more than this page can. Get in touch.