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 injournal_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, socost_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.
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 defaultactivity 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.Related
- The dual IFRS + TT200 chart - the chart this keeps small
- The 18 GL invariants - invariant 8
- Immutability and the hash chain - why dimensions are hashed
- Settlement, rails and the AP/AR spine - where
party_idis not optional