Skip to main content

Dimensions, not sub-accounts

What it guarantees

Analytical detail lives on the journal line, never as multiplied account codes. The chart stays small; the analysis stays arbitrary. And two dimensions - activity and flow - are hard-required on every line. A line that cannot say what activity it belongs to and whether it crossed a border does not post.

The six dimension columns

Every row in journal_entry_lines carries these: Tier 1 is enforced by GL invariant 8. Missing either raises DimensionRequiredError and nothing is written. Tier 2 is optional at the writer. Individual sub-ledgers impose their own requirements on top: settlement.record_obligation refuses a payable naming no supplier with MissingCounterpartyError, because an unattributable payable cannot be aged or chased.

Vocabulary and read coverage

The dimension catalog and its seeded definitions are built, so cost_object_id, flow and cost_nature are backed by a controlled vocabulary rather than bare journal-line columns. Analytical read coverage remains bounded: Accounting Analysis preserves visible rows and marks a missing journal API column as blocked instead of presenting an empty selector or silently widening an unattributed total.

Why Tier 1 is required rather than encouraged

activity and flow are the two axes every downstream statement needs and no downstream process can reconstruct.
  • Activity is what separates operating from financing in a cash-flow statement. Guessing it from the account code works until the first entry that hits an account used by both.
  • Flow is what separates domestic from cross-border for VAT and foreign-contractor tax. Guessing it from the currency is wrong the moment a domestic supplier invoices in USD.
Requiring them at the writer means the book can always answer those two questions. Making them optional means the book can answer them for the entries where somebody remembered, which is not an answer.

The alternative, and why it does not scale

The sub-account approach encodes detail in the account code:
The deciding argument is the fourth row. Sub-accounts fix the analysis axes at the moment the chart is designed. Dimensions let a question be asked that nobody thought of when the book was set up, which is most of the interesting questions.

Where it is enforced

Dimensions are part of the hash chain. _canonical_lines includes all six columns in the serialisation it hashes, so changing a line’s cost object after the fact would break the chain exactly as changing its amount would. A dimension is a fact about the entry, not an annotation on it.

How the agent path fills them

The Bookkeeper’s posting rules default activity to operating and derive flow from whether the line’s currency matches the org’s base currency (domestic when it does, cross_border when it does not). The resolved party from the match node rides along as party_id and party_type when there is one. Those defaults are deliberate and narrow. The agent is not guessing at an analysis axis; it is filling the two required fields with the value that is correct for the overwhelming majority of intake documents, and a human at the posting gate sees the proposed line including its dimensions before it posts.

The test that would fail if it broke

backend/tests/test_ledger_engine.py asserts that a line missing a Tier-1 dimension is refused. The sub-ledger suites assert the dimension values each family posts, which is what makes per-activity and per-flow reporting reproducible rather than incidental.

What goes wrong without it

Without required Tier 1, you get a book where 80% of lines carry an activity and the cash-flow statement is built from that 80% plus an assumption about the rest. Without dimensions at all, you get the chart-of-accounts explosion, and then the thing that always follows it: a spreadsheet on someone’s laptop that maps account-code fragments to the real analysis, maintained by hand, which is exactly the artifact GreatBook exists to replace.