Build1 publisher3 min readPublished
Three ledgers, one payment: reconciliation breaks because the model picked a winner
A dev.to essay argues the usual reconciliation fix, deciding which system is authoritative, destroys the evidence you need. The design should preserve the mismatch until something closes it.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- A processor says a payment settled, the product ledger shows the customer balance and fee entries, and the bank account still has no matching credit; none of those records is necessarily wrong, yet the payment is not reconciled.
- The mistake is asking which system has the true status; each system owns a different fact.
- Reconciliation belongs to the process that can compare those facts, preserve the mismatch and prove what happened at every boundary.
- A useful design sets invariants across the product ledger, the processor settlement record and the bank statement.
- The product ledger records the economic event the application accepted (capture, refund, fee, reserve movement, chargeback or correction) and should preserve the customer-facing and accounting consequences even when an external provider changes later.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A dev.to essay on payment systems engineering states the failure case plainly: a processor reports a payment as settled, the product ledger already shows the customer balance and the fee entries, and the bank account has no matching credit, yet none of those records is necessarily wrong and the payment is not reconciled [1]. That is worth dwelling on because it moves reconciliation out of the data-quality bucket and into the modelling bucket: the mistake, according to the piece, is asking which system holds the true status, when each system owns a different fact [2].
The three facts are not interchangeable. The product ledger records the economic event the application accepted, a capture, refund, fee, reserve movement, chargeback or correction, and it should preserve the customer-facing and accounting consequences even when a provider later changes its mind [5]. The processor owns which transactions and adjustments entered a payout [6]; Stripe exposes immutable balance transactions and keeps automatic payouts associated with the transactions they contain [7], and Adyen's settlement details report carries settled payments, fees, corrections, payouts and a payout reference that can surface on the bank statement [8]. Only the bank statement proves the cash moved, and it cannot explain the composition of a processor batch, just as a processor cannot prove a credit landed by marking a payment settled [9].
Collapsing the three into one mutable payment status is the actual defect. The call that moves money and the evidence that closes the books have different owners, so treating a provider response as all three facts makes the happy path simple and every exception ambiguous [10]. The author is explicit that a processor report or bank statement need not be a double-entry ledger; what matters is preserving each one as immutable evidence rather than flattening it [11]. With three evidence surfaces there are two adjacent boundaries to police, not one status field to update [21].
The construction is unglamorous: pick a currency and a reconciliation window, then write the equations that must hold once cutoffs, fees, reserves, refunds, chargebacks, corrections and foreign-exchange effects are accounted for [12]. The goal is not agreement at every moment, since these systems run on different clocks, but knowing which differences are expected, which deadline makes them actionable, and what evidence eventually closes them [13].
The worked example shows what the distinction buys. Do not reverse the customer entry because the bank credit is missing, since "payment settled" and "payout reached the bank" describe different boundaries [14]. Ask first whether the payment was in a closed settlement batch: if not, the first invariant may legitimately still be open on cutoff, reserve policy or status mapping; if it was, find the payout reference and compare the batch net against the expected credit [15]. The gap becomes a bank-boundary exception only after a provider-side completion fact exists and the agreed arrival window has passed, which is why the interim state should read awaiting_bank_evidence rather than a generic failed [16]. That routing matters operationally: a mapping defect wants a replay from immutable source events, a missing credit wants provider or bank investigation, and re-running the original payment fixes neither while risking a second economic event [17].
The output of a reconciliation run, in this design, is a durable object another worker or analyst can resolve without rebuilding the comparison from logs [18]. The sample record names the failed invariant, currency, window, the internal, processor and bank evidence present or absent, a status, an owner such as treasury_operations, and a next check time [19]. The detail that decides whether any of it works is identity stability across retries; otherwise each polling cycle mints a fresh exception and hides how old the break really is [20].