SPB Git

spb/air Public MIT

AIR — The Language of Accounting.

Python 100%
22.3 KB

# Double-Entry Ledger Engines and Event-Sourcing Accounting Patterns

Research date: 2026-08-05 Purpose: Survey the state of the art in programmable double-entry ledgers (TigerBeetle, Formance, Modern Treasury, Stripe Ledger, Increase, Square Books) and the foundational design patterns (Martin Fowler's Accounting Patterns, event sourcing) to inform the design of AIR's core (event model, provenance graph, compilation to journal entries, audit log).


# 1. Martin Fowler's Accounting Patterns

Fowler's Analysis Patterns (chapter 6) and his more up-to-date follow-up paper Accounting Patterns (martinfowler.com/apsupp/accounting.pdf, which supersedes the book chapter) define the canonical object model that nearly every modern ledger engine re-derives:

  • Accounting Event — something that occurs in business operations with financial significance. It is not an entry; it is the raw fact that triggers entries. This is precisely AIR's EconomicEvent concept: the event is the input, the journal entry is compiled output.
  • Posting Rule — the bridge between events and account impacts. A rule inspects an event and determines which accounts to debit/credit and by how much. Fowler treats posting rules as first-class, configurable objects (with variants: Individual Instance Method, Posting Rule Execution, Posting Rules for Many Accounts). This is the direct analogue of AIR's ALSL policies + the AIC posting pass.
  • Accounting Transaction — a multi-legged grouping of entries that must balance around a single business occurrence (two-legged is the special case; real transactions with taxes, fees and FX are multi-legged).
  • Entry — an individual amount posted to an account with a direction (debit/credit).
  • Account — aggregates entries by category (assets, liabilities, equity, revenue, expense).

Corrections without mutation. Fowler defines three adjustment patterns, all of which add entries rather than modify them:

  1. Reversal Adjustment — post offsetting (contra) entries to undo the original, then post the correct version.
  2. Replacement Adjustment — reverse and record corrected amounts in one operation.
  3. Difference Adjustment — post only the variance between original and corrected values.

Fowler emphasizes maintaining an "event trace": rather than modifying original entries, the system creates additional entries that preserve historical accuracy while achieving corrected balances — audit trail preserved, data integrity risks of in-place modification avoided. This maps 1:1 to AIR's planned incremental recompilation with auto-generated contra entries when a source document changes.

Sources:


# 2. TigerBeetle — a database whose schema is double-entry

TigerBeetle is a purpose-built OLTP financial database. Its central claim: the debit/credit model is minimal and complete — two entity types (accounts, transfers) plus one invariant (every debit has an equal and opposite credit) can model any exchange of value in any domain.

Key design properties relevant to AIR:

  • Debits/credits as first-class primitives. There is no generic row model; the fixed schema has ledgers, accounts, and transfers only. Business meaning is encoded in account/transfer codes, not free-form columns.
  • Strict append-only immutability. No UPDATE, no DELETE, no ALTER SCHEMA. Once a transfer is recorded it can never be erased; reversals are implemented as separate transfers, yielding a full, auditable log of business events and "effortless reconciliation."
  • Integer (fixed-point) amounts. All fields are fixed-size; amounts are unsigned integers (128-bit), with the currency scale defined per ledger. No floats anywhere in the hot path.
  • Invariant enforcement at write time. Accounts can be flagged so their balance may never go negative (debits_must_not_exceed_credits / inverse); violating transfers are rejected, not logged-then-fixed. Because transfers apply serially to current state, an accepted transfer is a proof the balance permitted it.
  • Linked transfers (atomic chains). A chain of transfers either all commit or none do — the primitive for multi-legged transactions (sale + tax + fee).
  • Two-phase transfers (pending → post/void) model holds/authorizations, i.e., the lifecycle stages AIR tracks in its provenance graph (Invoice → Payment → Settlement).
  • Externally validated: Jepsen tested TigerBeetle 0.16.11 for strict serializability.

Lesson for AIR: push balance invariants into the storage/compilation layer as hard rejections with diagnostics, not as after-the-fact checks; model every state change (including corrections) as a new immutable record.

Sources:


# 3. Formance Ledger and Numscript — programmable money movement

Formance Ledger (open source, Go) is a programmable double-entry accounting database: immutable, tamper-evident transaction log, built-in concurrency control, multi-asset support, and a DSL — Numscript — for modeling money movements.

  • Numscript = declarative DSL for transactions. A financial transaction is defined as a series of discrete value movements between abstract accounts (send [USD/2 100] (source = @world destination = @users:1234)). It replaces error-prone imperative code with readable, declarative, templatizable scripts. This is the closest existing analogue to ALSL: a small, deterministic language whose only job is to describe postings.
  • Determinism and conservation of money. Numscript programs are deterministic, always terminate with predictable output, and the language semantics guarantee no accidental creation or destruction of money and no currency-rounding leaks (allocations by percentage handle remainders explicitly). Execution is atomic: all postings commit or none.
  • Hash-chained log. The ledger chains transaction logs together (each transaction produces a hash over its data plus the previous hash), blockchain-style, giving tamper evidence — the exact mechanism AIR plans for its audit log.
  • Formance also published a useful formal model of double-entry for engineers (accounts, postings, balance as fold over postings).

Lesson for AIR: ALSL should aspire to Numscript-grade properties — declarative, deterministic, total (always terminates), money-conserving by construction, with explicit remainder handling in splits.

Sources:


# 4. Ledgers at scale: Modern Treasury, Stripe, Increase, Square

# 4.1 Modern Treasury ("How to Scale a Ledger", parts I–VI)

Modern Treasury sells Ledgers-as-an-API and documented its design in a six-part series. Core guarantees they identify as the product:

  • Immutability — all changes recorded so any past state can be retrieved; history must be replayable.
  • Double-entry enforcement — every movement names both source and destination; the API models everything with three core objects: Account, Entry, Transaction (Fowler's model, again).
  • Concurrency controls — prevent double-spending; entries can operate in authorizing mode (balance checks enforced synchronously, like TigerBeetle) or recording mode (log-first), chosen per Entry.
  • Idempotency — writes carry idempotency keys so client retries cannot double-post; combined with immutability this keeps data "pristine" under network failure.
  • Efficient aggregation — balances are derived, so they precompute/aggregate (Account Categories graph for roll-ups) rather than sum entries per read; scaling double-entry is hard precisely because history is immutable and must remain replayable at billions of transactions.

# 4.2 Stripe Ledger

Stripe's internal Ledger tracks and validates money movement across its Global Payments and Treasury Network:

  • An immutable log of events; producer systems are modeled as state machines whose behavior is expressed as logical fund flows — movements of balances between accounts.
  • Double-entry as mathematical proof of correctness: all platform activity is mapped to one common data structure, and traditional accounting principles validate the flows.
  • Scale/quality numbers: ~5 billion events/day; 99.99% of dollar volume fully ingested and verified within four days; data-quality platform achieves 99.9999%+ "explainability" of money movement — i.e., every cent is accounted for or flagged.
  • Key insight: the ledger is a verification and reconciliation layer over heterogeneous producer systems, not the systems themselves — very close to AIR's role as a common IR above heterogeneous ERPs.

# 4.3 Increase

Increase (banking-API bank) exposes a Bookkeeping API: clients create bookkeeping accounts (e.g., one per customer with compliance_category: customer_balance, plus commingled_cash accounts mapped to real bank accounts) and post bookkeeping entry sets whose entries must sum to match the transaction amount — balance enforced at the API boundary. It demonstrates the "compliance-labeled chart of accounts + balanced entry set" pattern for FBO/commingled-funds tracking.

# 4.4 Square Books

Square's internal service "Books" is an immutable double-entry accounting database service; their stated rationale: double-entry forces you to record not just what financial state change occurred but why, and all transactions must balance to zero.

Sources:


# 5. Event sourcing for financial systems

Event sourcing is the natural persistence model for accounting because accounting invented it: a bank ledger records every transaction and the balance is a derived sum.

  • Append-only event store. Events are only ever appended; never updated or deleted. Every state change is an immutable fact. The store is the single source of truth.
  • Projections / read models. Current state (account balances, trial balance, aging reports) is a materialized view built by replaying events. Projections are updated asynchronously and must be kept consistent with (and rebuildable from) the event store. For AIR: the compiled journal, the general ledger, and every financial report are projections of the EconomicEvent stream + policy versions.
  • Corrections via new events, never mutation. A wrong event is not edited; a compensating/reversal event is appended (matching Fowler's Reversal/Difference Adjustments and TigerBeetle's reversal transfers). Replaying the full stream still yields the corrected state, and the mistake itself remains visible for audit.
  • Why finance loves it: regulatory audit trails, fraud detection, time-travel ("what was the balance on March 31 as known on April 5?" — bi-temporal queries), recovery, and replay-based testing.
  • Pitfalls to plan for: schema/versioning of events (AIR's air_version + explicit migrations), projection lag (eventual consistency of read models), and event-store growth (snapshots).

# 5.1 Hash-chained, tamper-evident audit logs

Standard construction (used by Formance, Trillian/transparency.dev, and accepted by financial examiners):

  1. Append-only storage with a monotonic sequence number per row.
  2. Hash chaining: each entry stores prev_hash and own_hash = SHA-256(prev_hash || seq || canonical_encoding(fields)). A verifier replays the chain from genesis and reports the first break (gap, predecessor mismatch, or row-hash mismatch).
  3. Merkle trees on top for efficient inclusion proofs (chain verification alone is O(n)); periodically anchor the root somewhere hard to alter retrospectively (HSM-signed storage, WORM storage, external transparency log).
  4. Write-path discipline: atomic append + idempotent retries (outbox pattern) so network retries never create duplicate or missing entries; run continuous verification against the live chain, not only at export time.

Note the distinction: the audit log records who did what and when (SDK syscall journal); event sourcing makes domain events the primary source of truth. AIR needs both, and they are separate artifacts.

Sources:


# 6. Amounts: never floats; fixed-point/decimal and banker's rounding

# 6.1 Why floats are forbidden

  • IEEE-754 binary floating point cannot represent most decimal fractions exactly: 0.1 is stored as an approximation. Tiny representation errors compound over repeated operations; a worked example: $100 deposited daily at 6% interest compounded daily drifts ~$1.40 from the exact answer over one year using doubles.
  • In financial systems, small numeric errors become real discrepancies: balances that do not reconcile, reports that do not match fills, results that depend on operation order (float addition is not associative — fatal for AIR's determinism guarantee: same AIR + same policies must always produce identical entries).
  • Even the contrarian take (evanjones.ca, "You can use floating-point numbers for money") concedes it only works with extreme care and explicit rounding at every step — exactly the discipline that decimal types give you for free. Industry consensus and every ledger surveyed here (TigerBeetle, Formance, Modern Treasury, Stripe) use integers or decimals.

# 6.2 The correct representations

  • Fixed-point integers with explicit scale: store minor units (cents) or scaled integers (amount: i128, scale: u8) — TigerBeetle's approach (unsigned 128-bit integers, scale defined per ledger/asset).
  • Decimal types: rust_decimal (Rust), decimal.Decimal (Python), decimal (C#) — base-10 storage, exact cent representation, configurable rounding modes.
  • For AIR: JSON Schema should carry amounts as strings or {amount, currency} objects with decimal strings, never JSON numbers (JSON parsers commonly decode numbers as float64).

# 6.3 Banker's rounding (round-half-to-even)

  • Ties (exactly .5) round to the nearest even digit: 2.5 → 2, 3.5 → 4. Over many operations this removes the systematic upward bias of round-half-up, minimizing cumulative error. It is IEEE-754's default mode and decimal libraries support it (ROUND_HALF_EVEN).
  • Caveat: a rounding mode only behaves as intended if the underlying value is stored exactly in the first place — another reason floats are out.
  • Jurisdictional warning for AIR: rounding rules for taxes are legal, not stylistic — e.g., tax authorities may mandate round-half-up per line or per invoice total (CRA/Revenu Québec rules for GST/QST rounding must be verified separately; see the tax research doc before coding). Therefore the rounding mode must be a per-jurisdiction ALSL policy parameter, never hard-coded in the engine, and every rounding step should be a node in the provenance graph (Numscript's explicit remainder allocation is the model: when splitting an amount, the remainder cent is assigned deterministically and visibly).

Sources:


# 7. Design lessons for AIR

  1. Append-only event store as the single source of truth. EconomicEvents are immutable facts, only ever appended (PostgreSQL event store per CLAUDE.md §4). The general ledger, balances, and reports are projections rebuilt by replaying events through the compiler — like Stripe's Ledger sitting above producer systems. No UPDATE/DELETE on events or on compiled entries, ever (TigerBeetle discipline).

  2. Corrections as compensating entries, never mutation. When a source document changes or an error is found, append a new event; the AIC recompiles the delta and emits reversal (contra) entries + corrected entries automatically (Fowler's Reversal/Difference Adjustments; TigerBeetle reversal transfers; Modern Treasury immutability). The mistake stays visible in history — that is a feature.

  3. Balance invariant enforced at every stage, as a hard rejection. Assets = Liabilities + Equity (equivalently: every transaction's legs sum to zero) is checked after every compiler pass and at posting time; violations abort compilation with clang-quality diagnostics. Follow TigerBeetle: reject-at-write, don't detect-after-the-fact. Multi-legged transactions must be atomic (linked-transfer semantics: all legs or none). Add per-account invariants (e.g., cash accounts may not go negative) as ALSL policies.

  4. Provenance / traceability as a first-class graph. Every compiled entry links back to: source event → source document + OCR/LLM scores → policy versions applied → each intermediate transformation (tax computation, FX conversion, rounding step). Stripe's "99.9999% explainability" is the benchmark; Fowler's "event trace" and Square Books' "record why, not just what" are the pattern. Rounding and remainder allocation are explicit provenance nodes (Numscript model).

  5. Idempotency keys on every mutating syscall. CreateEconomicEvent, Post, Reverse, etc. accept a client idempotency key; retries are safe and never double-post (Modern Treasury; Increase entry sets). Backend post() must be idempotent per receipt as well — required for Phase 3 (QuickBooks OAuth/posting).

  6. Hash-chained audit log, separate from the event store. Every SDK syscall appends a row with seq, prev_hash, own_hash = SHA-256(prev_hash || seq || canonical_json); continuous verification recomputes the chain; periodically anchor a Merkle root to WORM/external storage (Formance log-chaining; Trillian; examiner-accepted practice per FinQub).

  7. Amounts are decimals/fixed-point end-to-end. Decimal strings in JSON schemas, rust_decimal/Decimal in code, integer minor units in storage. Rounding mode (banker's vs half-up) is a per-jurisdiction ALSL parameter, never engine-coded. Determinism (§5 of CLAUDE.md) is unachievable with floats.

  8. Keep the core model minimal: Account, Entry (debit/credit), Transaction, Event, Posting Rule. Every system surveyed — from Fowler (1996) to TigerBeetle (2026) — converges on these five objects. AIR's innovation is not the ledger model; it is the compiler (deterministic posting rules as versioned ALSL policies) and the IR (events, not entries, as the interchange format). Do not reinvent the ledger core; adopt it exactly and spend the novelty budget on passes, diagnostics, and backends.

  9. Two-phase / lifecycle-aware events. Pending → posted/voided states (TigerBeetle two-phase transfers; Modern Treasury authorizing vs recording modes) map to AIR's approval queue: events below the LLM-confidence threshold sit in pending and reserve nothing until approved.

  10. Balances are derived but must be cheap. Plan projection/aggregation tables (Modern Treasury Account Categories) so trial balances and per-account balances do not require replaying the full event stream on every read; snapshots + incremental projection updates.


End of research document — consulted 2026-08-05.