Design for the report you will need

Why account granularity is a reporting decision, and the cost of getting it wrong.

About 13 minutes

TheoryWhy this exists

It is December. You run a bakery that also does corporate catering, and you are deciding whether to keep the catering side. It takes staff time and a van, and you have never been sure it earns its keep.

You open the profit and loss statement. It says:

  • Sales — RM480,000
  • Wages — RM162,000
  • Ingredients — RM138,000
  • Motor expenses — RM31,000

Every figure is correct. None of them answers your question.

Re-reading the documents is not a fallback, it is a warning

The obvious move is to go find out. Filter the invoices, tag the catering ones, add them up. Then do the same for ingredients, which means opening supplier bills and deciding how much of a RM900 flour delivery went to catering rather than the counter.

Two things go wrong. The first is cost: 2,400 invoices and 700 bills, and you will spend a week on it. The second is worse — some of those documents genuinely cannot be split. That flour delivery was one purchase for one kitchen. The information about which loaves it became was never captured, and no amount of reading will recover it.

A question you cannot split at posting time is a question you cannot answer at reporting time. Not expensively — at all.

This is the fact that makes the chart of accounts a design problem rather than a filing convention. A report is an aggregation over accounts. It can slice along any line the accounts already draw, and along no line they do not. Whatever distinctions you fail to make when the transaction is recorded are distinctions that no longer exist.

So the instinct is to draw more lines. Split sales into counter and catering. Split ingredients too. Split wages. Split motor expenses into van and delivery bike. Keep going and you never get caught out again.

The opposite failure is quieter and worse

Here is what happens when you go too far.

You now have "Travel — client", "Travel — delivery" and "Motor expenses". A RM90 ride to a corporate tasting arrives on the card statement. Which account?

You could argue all three. Whoever posts it picks one. Next month, someone else picks a different one, or you pick differently because you are tired. Nobody is wrong, because there is no rule that decides it — the accounts overlap, and overlapping categories are resolved by whoever is holding the mouse.

A year later "Travel — client" reads RM14,200. That number looks precise. It is presented to two decimal places, it appears on a report, and it is roughly 70% meaningful. You will make a decision with it, and you will not know that you should have discounted it, because inconsistency leaves no trace in the total.

Compare the two failure modes:

  • Too coarse — you cannot answer the question. The gap is obvious. You feel it immediately, and you can fix the chart going forward.
  • Too fine — you get an answer, it is wrong, and it looks exactly like a right answer.

The second failure is more dangerous precisely because it is invisible. An empty cell prompts investigation. A confidently wrong number does not.

The test that resolves it

Granularity is not a virtue in either direction. The question to ask about any proposed split is not "would this be interesting to know" — almost everything would be. It is:

  1. What decision does this split feed? Name it concretely. "Whether to keep catering." "Whether to renew the van lease." If the only answer is "so we have the detail", you are adding maintenance cost for no return.
  2. Can you state a rule that assigns every transaction to exactly one of these accounts? Not a rule you would follow — a rule a new bookkeeper on their second day would apply the same way you do. If you cannot write that sentence, the split will not survive contact with the actual bookkeeping.

Splits that pass both tests are worth having. Splits that fail the second one are worse than no split at all.

Why the timing is asymmetric

One more thing decides how much care this deserves up front. Splitting an account later is easy to do and hard to use. You create "Sales — catering" in June, and now you have five months of blended history and seven months of split history. Every year-on-year comparison crosses that seam. You either restate the old months by hand — which requires the source detail you did not capture — or you live with a chart that changes shape mid-year and comparatives that mean two different things.

Consolidating accounts later is cheap: totals add. Separating them later is not, because you cannot un-blend a number.

Which gives the practical bias. Be generous where you already know a decision depends on the split, and conservative everywhere else — one account you trust beats four you have to caveat.

PracticeDoing it in Cynco

A café owner asks a reasonable question: coffee or pastries — which one actually makes money?

Her chart has one revenue account, 4000 Sales, and one cost account, 5000 Cost of sales. The P&L is correct and useless for this. Here is how the two available fixes differ.

Option A — split the accounts

In Accounting → Chart of accounts, add:

  • 4010 Sales — beverages
  • 4020 Sales — food
  • 5010 Cost of sales — beverages
  • 5020 Cost of sales — food

From now on, sales get posted to one or the other, and the gross margin on each line is readable straight off the P&L with no extra work.

What it costs her: every sale now needs a decision, and some are genuinely blended — a RM18 breakfast set with a flat white in it. She needs a standing rule ("sets post to food") and she needs it written down, or the split degrades. Her cost of sales split is also only as good as her supplier bills allow: a RM240 dairy order feeds both sides of the counter, and she will be allocating it by judgement.

Option B — one account, a second axis

Keep 4000 Sales and classify each transaction along a second, independent field — a dimension, tag, class or tracking category, depending on the system. The account says what kind of thing the amount is. The dimension says which part of the business it belongs to.

Reports can then be filtered or grouped by that field, so she gets the same beverages-versus-food view without four new accounts. It also composes: add a second location later and she can cut by location, product, or both, with no chart changes at all.

What it costs her: the dimension has to actually be filled in. A blank dimension is not an error the way an unbalanced entry is — it posts fine and quietly drops out of every grouped report. Check what your Cynco setup makes available here before designing around it; the mechanism and its name vary, and a plan that depends on a field you do not have is not a plan.

Which to pick

Use accounts when the distinction is stable, small in number, and something you want visible on the face of the statement without anyone remembering to filter. Beverages versus food for a café passes: two values, they will not change, and it is the main thing she wants to see.

Use a dimension when the values are numerous, likely to change, or cut across many accounts — locations, projects, product lines, sales channels. A new location should not require twelve new accounts.

Try it yourself

Fifteen minutes, and it will tell you more about your chart than any review:

  1. Write down three decisions you expect to make in the next twelve months. Real ones: renew a lease, drop a product, hire, raise a price.
  2. For each, write the exact figure you would want in front of you.
  3. Open Reports and try to get that figure. Not approximately — the actual number.

Every one you cannot get is a distinction your chart is not drawing. Now apply the two tests from the theory section before you add anything: name the decision, and write the one-sentence rule that tells a new bookkeeper which account a given transaction goes to. If the rule will not write, the split is not ready.

TechnologyHow it is built

The choice from the practice section — split the account or add a second field — is a database modelling problem wearing accounting clothes. It is the same decision as whether to encode a composite key into one column or normalise it into several, and it fails in exactly the same way.

Encoding every combination into the account code

Suppose the business sells 5 product lines, in 3 regions, through 4 channels, and wants revenue readable by any of them. Encode all of it in the chart and you need one account per combination:

text
4110  Sales — coffee, KL, retail
4111  Sales — coffee, KL, wholesale
4112  Sales — coffee, KL, online
...

5 × 3 × 4 = 60 revenue accounts. Then marketing opens a fifth region and it is 80. Add a fourth channel and it is 100. The chart grows as the product of the axes, and every one of those accounts has to be created, mapped in whatever rules drive automatic posting, and placed in the statement layout.

The deeper problem is not the count. It is that the axes are no longer separately queryable. "Total coffee revenue across all regions" requires knowing which of the 60 codes contain coffee, which means either a naming convention parsed by string matching, or a lookup table that maps codes back to the three attributes you flattened into them. You destroyed structure at write time and are now reconstructing it at read time — which is the definition of a denormalisation you should not have made.

Two axes, two fields

Model the axes as what they are:

sql
create table journal_entry_lines (
  id            uuid primary key,
  entry_id      uuid not null references journal_entries(id),
  account_id    uuid not null references accounts(id),
  product_id    uuid references products(id),
  region_id     uuid references regions(id),
  channel_id    uuid references channels(id),
  debit         numeric(19,4) not null default 0,
  credit        numeric(19,4) not null default 0
);

Now the chart needs one revenue account. 5 + 3 + 4 = 12 dimension values instead of 60 accounts, and each axis is independent:

sql
select r.name, sum(l.credit - l.debit)
from journal_entry_lines l
join accounts a on a.id = l.account_id
join regions  r on r.id = l.region_id
where a.type = 'income'
group by r.name;

Swap region_id for product_id and you have the other cut, with no new accounts and no schema change. Adding the fifth region is inserting one row.

There is a second reason this matters: the 60-account version is a sparse matrix. Most combinations never occur — you do not sell every product in every region through every channel — so you carry dozens of permanently empty accounts that still appear in dropdowns, still need mapping, and still let someone post to a combination that does not exist.

The two properties that decide it

When you are unsure whether something is an account or a dimension, look at cardinality and stability.

Accounts should be few and stable. They are referenced by everything: statement layouts, tax mappings, the posting rules behind invoices and bills, budgets, prior-year comparatives. An account is closer to a column than a row — changing the set changes the shape of every report built on it. Charts of a few hundred accounts are normal; charts of several thousand are a symptom.

Dimensions should be many and changeable. They are rows in a lookup table. Creating, renaming or retiring one is data entry, not a structural change, and reports adapt because they group by whatever values exist.

If adding one more value to a category would require creating several accounts, that category is a dimension.

The failure mode dimensions introduce

Dimensions are nullable in the schema above, and that is the trap. An unbalanced entry is rejected — the invariant is enforced and loud. A line with product_id = null posts perfectly, then silently vanishes from every report that groups by product. Your revenue by product sums to RM418,000 while the P&L says RM480,000, and the RM62,000 gap is untagged lines nobody noticed.

So the requirement has to be encoded somewhere, not left to habit. The workable shape is a per-account rule — "income accounts require a product dimension" — checked in the same place the balance check lives: the single write path every ledger posting goes through. That way imports, integrations and document-generated postings are all held to it, not only the people typing into a form.

And build the reconciliation anyway. Grouped totals should be compared against the account total, with the difference shown as an explicit "unassigned" line rather than dropped. A report that quietly omits rows is worse than one that admits it has a gap.