Double-Entry & the Payroll Journal
Money does not appear or vanish. It moves. Everything in accounting follows from taking that seriously: if an amount showed up somewhere, it came from somewhere, and a record that shows only the arrival is a record of half an event.
That is double-entry bookkeeping, and it is roughly five hundred years old. Every movement is written twice, once as where it went and once as where it came from, and the two sides must agree. If they do not, something is missing, and the system tells you immediately rather than at year end.
You need this before we build anything that pays people, because the moment we generate a bank file we are not calculating any more. We are moving money, and money movement is an accounting problem first.
Debits, credits, and why the words are confusing#
The vocabulary is the only genuinely hard part, and it is hard because the words do not mean what they mean in ordinary speech. A debit is not bad and a credit is not good. They are just the two sides, left and right.
The rule that makes it usable: a debit increases what you own or what you have spent, and a credit increases what you owe or what you earned. Every transaction has at least one of each and they sum to the same amount.
An account is a bucket in the chart of accounts, and there are five kinds: assets, liabilities, equity, income and expense. That is all the machinery there is.
The payroll journal#
When a payroll run is finalised, the company has not paid anyone yet. It has incurred the cost and taken on obligations. That distinction is the whole reason payroll needs two entries rather than one.
On payroll finalisation, the gross cost becomes an expense, and everything the company now owes becomes a liability:
| Debit | Credit | |
|---|---|---|
| Salary expense (gross, plus employer contributions) | ✓ | |
| PPh 21 payable to the tax office | ✓ | |
| BPJS payable, employee and employer portions | ✓ | |
| Net pay payable to employees | ✓ |
Nothing has left the bank. The company simply now owes three different parties, and the total of what it owes equals the cost it recognised.
On payment day, each liability is settled and cash goes down:
| Debit | Credit | |
|---|---|---|
| Net pay payable | ✓ | |
| PPh 21 payable | ✓ | |
| BPJS payable | ✓ | |
| Cash at bank | ✓ |
Two consequences worth holding onto. Between those events the company genuinely owes money it has not paid, and that is a real number a finance team needs. And the tax and BPJS liabilities usually clear on a different date from the salaries, which is why they are separate accounts rather than one lump.
Ledger rules that are not optional#
These are the rules that make a ledger trustworthy. They are learned expensively by everyone who skips them.
Entries are append-only. You do not edit a posted entry, ever. A mistake is corrected by posting a new entry that reverses it and a further entry that states the truth. The wrong entry stays visible forever, because the record of what was believed at the time is part of the audit trail.
Balances are derived, never stored. A balance is the sum of its entries. Storing a balance and updating it alongside creates two sources of truth that will eventually disagree, and when they do there is no way to tell which is right.
Money is integer minor units. Rupiah, never floats. This is already how our Money value object works and the reason is that floating point silently loses fractions, and lost fractions in money reappear as an unexplainable difference nobody can find.
Every movement carries an idempotency key. If an instruction is submitted twice, whether by a retry, a double click, or a network timeout, the second attempt must be recognised as the same instruction and do nothing. This is the single most important rule on this page, because the failure it prevents is paying everybody twice.
No response is not the same as failure. When a bank connection times out, we do not know whether the payment happened. Treating that as a failure and retrying is how a company pays twice; treating it as success is how people go unpaid. It is a third state, and it has to exist in the model and be resolved by asking the bank rather than by guessing.
The line we must not cross#
Moving money in Indonesia is a licensed activity. Bank Indonesia regulates payment services, and there are defined categories of payment service provider. We do not hold customer funds. Disbursement is instructed from the customer’s own bank account, and the money never rests with us.
That is not a technical preference. Holding funds without the appropriate licence is a regulatory problem of a different order to anything else in this product, and the architecture has to make it structurally impossible rather than merely avoided by convention. Anything in this area gets legal input before design, not after.
Why this matters to our strategy#
Our north star counts payroll periods paid end to end with zero manual intervention, and the last two words of “end to end” are disbursement. A run that produces a correct payslip and then a bank file somebody edits by hand does not count, so this is not an area we can defer indefinitely without capping the number we measure ourselves by.
It also decides whether our core claim survives contact with money. We are selling an explanation: every number traceable, every movement attributable. A mutable ledger destroys that in one step, because the moment an entry can be edited, the history is only as good as the last person who touched it, and “the system says so” is exactly the answer that sends a customer back to the spreadsheet.
And there is an asymmetry worth being explicit about. A wrong payslip is embarrassing and fixable next month. A double payment is a company’s cash gone and their trust with it, and it is the sort of failure that ends a payroll vendor rather than merely damaging one. That asymmetry is why the ledger rules above are prerequisites rather than improvements.
What this means for what we build#
- Learn this before writing disbursement code, not during. The competence test in Domain Standards is deliberately a paper exercise: journal a full run, produce the instruction, and reconcile a statement containing a rejected row back to zero.
- The ledger is append-only from the first commit. It cannot be retrofitted, because by the time you need it you already have history that was edited.
- Idempotency keys on every instruction, generated where the instruction originates, and honoured on retry.
- Model the unknown state. Instructed, submitted, settled, returned, failed, and unknown. Resolve unknown by querying, never by assuming.
- Reconcile automatically and surface what did not match. A partial batch rejection is normal and must produce a clear list of who was not paid, not a silent success.
- Never let us hold funds. Instruct from the customer’s account, and design so that any other arrangement would require a deliberate architectural change rather than a configuration one.
Related#
- The Payroll Cycle is where disbursement sits, and why the point of no return matters.
- Gross to Net produces the amounts this journal records.
- Domain Standards & Learning Map has the full payments learning list and its competence test.