# Double-entry posting: balanced does not mean correct

Original LockedIn Labs workshop · 50 minutes. Python 3.11+ standard library.
Follow rows, sums and a SQL transaction first. The program creates a fresh
in-memory SQLite ledger for each fictional case and closes it afterward. No
network, bank, payment adapter, account credentials or persistent database is
used. All amounts are integer USD minor units; 12500 means USD 125.00.

You will distinguish bookkeeping vocabulary, balance, business meaning,
transaction completeness, duplicate prevention and explicit correction history.
The [financial reconciliation guide](/training/guides/financial-reconciliation)
compares case and credit extracts. This workshop inspects the posting beneath
those extracts; it does not implement a payment system or determine real policy.

## 1. Learn the accounting words before choosing fields

A **journal** groups the entry lines that record an event. A **ledger** organizes
postings by account. **Debit** and **credit** name opposite accounting sides;
they do not universally mean loss and gain. In double-entry bookkeeping, posted
debit totals and credit totals must agree. [IRS Publication 583](https://www.irs.gov/publications/p583)
provides an introductory primary reference for journals, ledgers and equal sides.
The exercise supplies its own fictional chart and posting instruction, rather
than deriving a real accounting policy from that general rule.

Our chart is deliberately small:

| Account | Fictional meaning | How this fixture records it |
| --- | --- | --- |
| `cash` | Book cash asset | Debit increases its net debit; credit decreases it |
| `receivable` | Amount owed on a booked invoice | Debit increases it; a collection credits the matching party |
| `revenue` | Already-booked service revenue | Credit creates a negative net-debit value in this report |

A **trial balance** compares account debit and credit totals. Our
`trial_net_debit_minor` is their signed difference: positive amounts are net
debits and negative amounts are net credits. A zero difference is an arithmetic
check, not evidence that the account, party or source event was right.

A **subsidiary ledger** preserves detail beneath a control account, such as
receivables by customer. The fixture represents this as separate A and B buckets.
It is not a full customer ledger, chart of accounts or reporting standard.
A credit balance in B's bucket is recorded explicitly; the program does not
silently relabel it as a valid settlement of A's invoice.

## 2. State the collection target independently

Every case starts with a supplied already-booked invoice for party A:

```text
Opening journal INVOICE-A:
  Debit receivable A 12500
  Credit revenue     12500

Independent supplied instruction PAY-1:
  Collect that existing invoice once for A.

Expected final net-debit buckets:
  cash:          12500
  receivable_A:      0
  receivable_B:      0
  revenue:      -12500
```

The opening invoice is a fixture assumption. This lesson does not decide when
a real service may recognize revenue, whether a payment has settled, or which
business unit owns a source. Cash here is a ledger account, not a read of a bank
balance. A collection should reduce this existing receivable rather than book
that revenue a second time under the supplied instruction.

Read `instruction` and `expected_net_debit_minor` separately from actual rows.
The expectation is not derived from a successful insert or a zero trial sum.
The six visual frames explain eight independently reset SQL cases; the browser
does not execute the ledger or move money.

## 3. Run eight actual posting cases

Download and read [the runner](/templates/fde-ledger-posting.py). Keep an untouched
reference. Predict the trial difference and whether the independent collection
target matches before reading JSON output:

```sh
python3 --version
python3 fde-ledger-posting.py > ledger-12500.json
python3 fde-ledger-posting.py --amount-minor 12501 > ledger-12501.json
```

The changed amount is one minor unit larger. Accepted amounts are integers from
1 through 1000000; the fixture makes no currency conversion or rounding decision.
Use the [money-contract workshop](/training/guides/money-contracts) for units,
currency identity, precision and explicit rounding questions.

| Case | Actual trial net at default amount | What remains wrong or unresolved |
| --- | --- | --- |
| `correct` | 0 | Local buckets match the supplied target; external source is untested |
| `wrong-account` | 0 | A still owes 12500; revenue has been credited twice |
| `wrong-party` | 0 | A still owes 12500; B has a credit 12500 despite aggregate receivables zero |
| `duplicate-naive` | 0 | Cash is 25000 and A has a credit 12500 after two balanced receipts |
| `duplicate-guarded` | 0 | One receipt; identical replay returns it and changed-party payload conflicts |
| `partial-naive` | 12500 | First debit remains committed without its credit |
| `partial-atomic` | 0 | Only the opening invoice remains; the collection is still unposted |
| `reversal` | 0 | Original, inverse and corrected history remain; no real payment operation occurred |

A successful process exit only means the report was produced. Inspect
`post_results`, `journals`, `entries`, `actual_net_debit_minor` and
`business_differences`. An intentionally bad case can exit successfully while
reporting a false collection match. That is counterexample evidence, not a pass
for a learner or a real financial operation.

## 4. Find the balanced error in the actual rows

Use this written ledger alongside your original prediction:

```text
wrong-account:
  receipt: debit cash 12500 / credit revenue 12500
  every_journal_balanced: true
  trial_net_debit_minor: 0
  receivable_A: 12500        # not cleared
  revenue: -25000           # opening invoice plus wrong credit

wrong-party:
  receipt: debit cash 12500 / credit receivable B 12500
  receivable_A:  12500
  receivable_B: -12500
  aggregate receivables: 0  # conceals the party mismatch

partial-atomic:
  post result: rolled_back / injected_after_first_line
  remaining journals: opening invoice only
  trial_net_debit_minor: 0
  cash: 0 / receivable_A: 12500
  matches_supplied_collection_target: false
```

Wrong-party demonstrates why a general-ledger account total can agree while its
underlying business allocation is wrong. Do not drop party identity when building
a reconciliation extract. Wrong-account demonstrates why equal amounts and
equal sides cannot choose the correct classification. These are distinct gaps
for the responsible source and accounting owners.

In the atomic partial case, rollback preserves the opening state. It does not
complete the collection or authorize another attempt. The naive partial case
uses SQLite autocommit for individual inserts, then deliberately raises after
the first entry. The atomic case instead runs `BEGIN IMMEDIATE`, inserts the
journal/lines and rolls the same exception back. This is a controlled local
exception and real SQL rollback, not a process kill, concurrent-worker test or
unknown remote commit outcome.

## 5. Preserve operation identity and correction history

The transactional writer checks at least two entry lines and a zero sum before
committing. Its receipt binds an operation key to the exact normalized posting
payload and journal ID. In duplicate-guarded, the outcomes are:

```text
first PAY-1:        applied  -> journal 2
identical PAY-1:    replayed -> journal 2
PAY-1 changed to B: conflict -> journal 2 unchanged
```

The payload retains source reference, journal kind, ordered lines, account,
party, side, amount and any reversal reference. The operation receipt and
posting commit together. A key alone is not enough to accept a different intent.
The naive comparison deliberately skips this receipt boundary while still
checking balance, so two balanced postings can represent duplicate work.
No source reference or operation label authenticates a caller.

For reversal, inspect all three snapshots: the initial wrong posting, the state
after its inverse, and the final correction. Journal 2 remains unchanged.
Journal 3 points to journal 2 with `reverses_journal_id` and swaps each original
side. That intermediate state returns to the opening invoice balances. Journal
4 separately records the intended collection with a new operation key and the
same source reference. All four journals, including the opening invoice, remain.

This writer preserves history through inserts; the database is not a tamper-proof
or immutable ledger. The fixture permits at most one reversing journal for an
original and implements no partial-reversal workflow or production approval.
Correcting these book entries does not refund, collect or undo real cash at
another service. An authorized workflow owner must define those separate actions.

Read the small schema and `post`. Account names, sides, positive integer amounts
and receivable parties have database checks. Journal balance and exact receipt
binding are enforced by this writer inside its transaction. Direct privileged
SQL can bypass application rules. This one-connection in-memory exercise tests
no production permissions, concurrent posting, durable recovery, currencies,
period close, accounting standard, tax rule or real payment rail. Use your
organization's reviewed ledger and posting contract rather than this fixture.

## 6. Receive the posting as a financial-services pod

Rotate three roles across a changed run:

- **Builder:** retain original command, runtime, source instruction, exact journal
  and entry IDs, post status and account/party residuals. Locate the transaction.
- **Receiver:** change the amount by one minor unit without author takeover.
  Reproduce one balanced wrong result and the atomic still-unposted result.
  Trace a reversal without deleting the original evidence.
- **Source/accounting and application owners:** agree the source identity,
  account and party mapping, operation scope, correction authority and real
  receiving-adapter evidence. Retain any unresolved business action explicitly.

Each participant keeps an individual record:

```text
Learner / run / exact command / Python and SQLite versions:
Prediction before execution:
Source instruction / currency / amount unit:
Actual journal and entry IDs / operation receipt:
Debit and credit totals / actual party-level buckets:
Independent expected buckets / specific residual:
My contribution / assistance received:
Changed amount / prediction / observed result:
Receiving action actually attempted / assistance:
Remaining action / accountable owner / next bounded source or adapter check:
```

For a real receiving exercise, use an approved synthetic adapter and agreed
posting contract. Ask an accounting owner to explain one mismatched classification
or customer residual. Then identify the exact external source evidence still
needed before calling a payment complete. Neither local trial equality nor the
fictional target match supplies that evidence.

## 7. Defend the boundary in an interview

**Explain:** Why can two balanced journal entries still be duplicate work?
Use the naive PAY-1 receipts and cash 25000 beside the original 12500 target.

**Debug:** Why does aggregate receivables zero fail to settle A? Locate both
party buckets and the actual credit line, rather than proposing a new tolerance.

**Design:** Describe the independent posting instruction, atomic validation,
scoped operation identity, original/correction lineage, source authority and
receiving test. Distinguish book correction from an external compensation.

Primary references checked 28 October 2026:
[IRS · Bookkeeping systems](https://www.irs.gov/publications/p583),
[Python SQLite transactions](https://docs.python.org/3.12/library/sqlite3.html#transaction-control),
and [SQLite transactions](https://www.sqlite.org/lang_transaction.html).
The chart, synthetic invoice, posting cases, visual frames and receiving exercise
are original instruction, not an accounting policy or tax recommendation.
