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 · 48 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 Revenue1,181.429
2300VAT Output Tax59.071
5000Cost of Goods Sold742.000
1300Inventory742.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 stock revaluation, a payroll run — 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 · payroll2Rules 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.

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 48 transaction types

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

  • Sale
  • SalesReturn
  • SaleAdjustment
  • SaleDiscountChannelAdjustment
  • Purchase
  • PurchaseReturn
  • PurchaseInvoicePost
  • PurchaseInvoiceVoid
  • COGS
  • Payment
  • Receipt
  • SupplierPayment
  • SupplierAdvance
  • SupplierRefundPayment
  • SupplierCreditRefund
  • SupplierWriteOff
  • CustomerAdvance
  • WriteOff
  • InventoryAdjustment
  • InventoryAdjustmentVoid
  • InventoryRevaluation
  • StockInTransit
  • StockInTransitReceive
  • StockInTransitWriteOff
  • StockReceive
  • StockReceiveVoid
  • OperatingExpense
  • AccruedExpense
  • Payroll
  • Depreciation
  • FixedAsset
  • Dividend
  • TaxDeduction
  • ForexGainLoss
  • RoundingDifference
  • CashShortageSurplus
  • CashDeposit
  • CashTransaction
  • BankTransaction
  • InterAccountTransfer
  • SafeDrop
  • RegisterOpen
  • RegisterClose
  • CommissionAccrual
  • CommissionPayment
  • CommissionReversal
  • PromotionalGiveaway
  • Suspense
  • Suspense — the honest fallback
  • Reversal counterpart

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