Designing double-entry: debits, credits and a reconcilable voucher
A practical design for account boundaries, balanced entries, multiple currencies, idempotency and reversals in a business ledger.
“Debit” and “credit” are easy to misread as two buttons for income and spending. They are directions in an entry. Their meaning comes from the account type, account owner and business rule.
This article does not replace accounting standards or a company’s chart-of-accounts policy. It uses product and engineering language to draw a ledger boundary that can be implemented: which events may post, how entries are generated, how they can be rebuilt and when an unknown result must remain unresolved.
Debits and credits are not plus and minus
For common account types, start with this simplified table of the normal increase direction:
| Account type | Debit increases | Credit increases | Typical example |
|---|---|---|---|
| Asset | Yes | No | Bank deposit, recognized receivable |
| Liability | No | Yes | Merchant payable, customer pre-funding obligation |
| Equity | No | Yes | Owner contribution, retained result |
| Revenue | No | Yes | Service fee meeting recognition conditions |
| Expense | Yes | No | Provider fee, operating expense |
The table describes normal balance direction. It is not a flowchart saying that money moves from left to right. When a platform receives customer funds, an asset may increase by debit while the liability to the customer increases by credit. When it pays a merchant, the asset decreases by credit while the merchant payable decreases by debit.
The useful question is therefore not only “is this income or spending?” Ask who controls the funds, who owes whom and which accounts this event changes.
Define accounts and ownership first
A chart-of-accounts code classifies the ledger. A subaccount records a particular owner’s activity within that classification, and its balance is derived from entries. The classification, account and balance each have a separate purpose.
A payment system usually needs separate boundaries for platform-owned funds, customer funds, merchant payables, provider clearing, fee revenue, refund payables and suspense or reconciliation accounts. Exact names depend on the entity and accounting policy; these are modeling examples.
Give every account a stable identity and explicit boundary:
- accountId: unchanged when a display name changes.
- ownerType / ownerId: platform, customer, merchant or external clearing party.
- accountType: asset, liability, revenue, expense and so on.
- currency: the original balance currency, never inferred from the current page.
- status: active, frozen, closed or under reconciliation.
- externalRef: an external account or settlement reference, nullable but never invented.
The account itself should not accept arbitrary page requests that edit a balance. A business service emits an event. A ledger service uses a versioned mapping to create a voucher and writes immutable entries. A balance can be cached, but the cache is not the source of truth.
Table names are illustrative:
| Record | Main fields | Key constraint |
|---|---|---|
| business_event | Source, business-event reference, stage, event time, evidence digest | Retain deliveries and original evidence |
| journal | Voucher ID, book, event reference, rule version, posting time | Post each eligible business stage once |
| entry | Voucher ID, line number, account, debit/credit, amount, currency | Unique line within a voucher; exact positive amount |
| ledger_account | Owner, account code, currency, normal balance side | Explicit entity and book boundary; status history |
| balance_projection | Account, debit/credit totals, last processed entry | Rebuildable; readers know the projection cutoff |
Within one controlled database, commit the voucher, all entries, idempotency record and balance or hold changes used for funds decisions in one transaction. Cross-service notifications can use a transactional outbox with idempotent consumers. A remote provider cannot join the local database transaction; its outcome converges through a state machine, queries and reconciliation.
Keep every voucher balanced
A voucher confirms one business stage in the ledger. An order can generate separate receipt, fee and settlement vouchers. Each needs a unique number, event type, event time, posting time, source, related order and entries. Each entry needs at least an account, debit or credit direction, amount, currency and precision.
This example uses a business ledger balanced separately per currency: within each voucher, book, entity and currency, total debits = total credits. The single-currency accounting equation is assets = liabilities + equity; revenue less expenses forms current-period profit and is closed according to policy. A debit-normal balance equals opening balance + period debits − period credits. A credit-normal balance uses the opposite order.
A simplified posting check is:
- There are at least two entries, with both a debit and a credit.
- Group the voucher by book, entity and currency; total debits equal total credits. Use each entity’s due-to/due-from accounts for inter-entity movements.
- The amount is positive, the currency is valid and the account permits posting.
- The business-stage idempotency key, voucher ID and line number within the voucher satisfy their respective uniqueness rules.
- Commit the whole voucher once all checks pass; a failure makes the voucher ineffective.
A 1,000 CNY receipt and a 30 CNY fee can be split into different vouchers. The point is not to force every business event into two lines. The point is to make the meaning clear, keep amounts balanced within the defined scope and preserve references that reconstruct the full process.
Enforce balance checks at the posting boundary, rather than only in a page or daily report. Do not fill a difference with “other revenue” or a generic adjustment account. Investigate it through reconciliation and create an adjustment only with evidence and permission. Writing equal amounts on two sides cannot prove the business event is correct.
Multiple currencies, precision and rates
Multiple currencies add a scope question to “debits equal credits.” USD 100 and EUR 92 cannot be added directly, and equal valuation under one rate does not prove that the original-currency books reconcile.
Keep at least these layers separate:
| Layer | Preserve |
|---|---|
| Transaction | Original amount, currency, order and external reference |
| Ledger | Entries, accounts and balances per currency |
| Valuation | Functional currency, rate source, timestamp, precision and rounding |
| Fee | Fee currency, payer and the originating event |
A conversion can use linked USD and EUR entries, or bridge and clearing accounts to represent funds in transit. Either model must say which side is confirmed, which side is unknown and how the rate and fee are posted.
For an internal business-account reclassification only, suppose a customer has a 100 USD payable balance and exchanges it for a hypothetical 92 EUR, without fees. The USD voucher debits customer USD payable 100 USD and credits USD FX bridge 100 USD. The EUR voucher debits EUR FX bridge 92 EUR and credits customer EUR payable 92 EUR. Each currency balances separately and both vouchers share a conversion reference. Bridge accounts represent the platform’s currency position or transit relationship; reconcile them against actual positions, external delivery and fees. They are not assets created from nothing.
Statutory or functional-currency reports must separately recognize FX gains, losses and rounding under the applicable policy. This business subledger example does not replace a financial general ledger.
Do not accumulate money with binary floating point. Use exact decimal arithmetic or each currency’s defined minor unit, and record every rounding step in a reproducible rule. A display valuation must not replace the original transaction amount.
Idempotency, periods and reversals
Ledger services receive retries, duplicate callbacks and out-of-order events. Scope the idempotency key to the book, source, stable business-event reference and posting stage. Partial captures and refunds need their own subevent references. A delivery ID may change on every retry; it is not a substitute for the business reference. Preserve the rule version as historical evidence, without treating a version change as permission to post the same event again.
Combine database uniqueness with controlled state transitions. Reading “not found” before inserting is not enough under concurrency. The same key and payload return the committed result; the same key with a different amount or currency is rejected and logged as a conflict. Duplicating a whole balanced voucher still balances, so these controls and reconciliation are necessary.
Never edit a posted voucher in place. Preserve it, then create a reversal or adjustment voucher under the original business rule and link the original number. Partial refunds, partial capture and fee corrections need cumulative relationships and limits.
Posting time and business event time can differ. Once a period is closed, do not silently rewrite history. A cross-period correction should use an allowed current period and retain its reason, approver and linked voucher. When reading a balance, state whether it is real-time unclosed, posted or as of a cutoff time.
Preserve a pending event after a timeout or unknown external outcome. If bank arrival is evidenced but ownership is unclear, an approved policy may debit bank and credit an unidentified-receipts or suspense account. If even the movement of money is uncertain, retain the event and hold state without inventing a final funds voucher. Preserve earlier confirmed entries and use new evidence to advance the next stage.
Preconditions and acceptance effects
Before building double-entry, establish at least:
- The money-owning parties, account ownership, currencies and functional-currency scope.
- Which business states can create vouchers, and how authorization, capture, clearing, arrival and refund post.
- Versioned account mappings, fee rules, exchange-rate and rounding rules.
- Stable event identifiers, idempotency constraints, permissions and period-closing controls.
- External reconciliation data, an exception owner and queryable audit records.
Migration also needs reconciled opening balances. Agree one cutoff by entity, account and currency; inventory transit funds, holds and unresolved transactions; create balanced opening vouchers with evidence. Do not push every difference into an opening-adjustment account to pass a trial balance. Reconcile in shadow first, then define a cutover time and one authoritative writer so the old and new systems cannot post twice.
Acceptance should cover: rebuilding the three example vouchers to bank 30 CNY and payable zero; replaying an event 100 times with one posting; at most one successful hold when two concurrent requests reserve 60 from 100 available; all-or-nothing posting on interruption; no unauthorized writes to a closed period; no early increase of available funds for unknown outcomes. These are acceptance targets for a ledger implementation; this website does not operate a funds ledger service.
Once it works, the team should answer four questions from one record: where did the money come from, who owns it now, why did it change and how can we prove it was not omitted or duplicated? Executable checks include rebuilding a random business event to its voucher, replaying the same event without a new balance change, matching recomputed balances per account and currency, keeping the original record after a reversal and preventing reports from treating pending state as settled.
Start with why double-entry fits business systems better than one-sided records, then use the flow from a business event to its final posting to check implementation order.