← Back
Home › Systems › Finance and trading

reconciliation

Compares records that should describe the same money, matches what agrees, and turns every difference into a tracked exception.

Modern Treasury

Open the reconciliation pagereconciliation Hdr histogramdata-structures: Log-linear bucketed histograms with bounded relative error for latency percentilesBatch checkpoint restartdistributed: Large batch jobs (end-of-day, settlement, payroll runs) that checkpoint and restart idempotentlyDeterministic simulationtestingdistributed: Run the whole system in one thread under a seeded scheduler with simulated network, disk and clock, so every failure replays from its seed.›Primary-backup replicationdistributed: One node orders all writes and ships them to standbys; the acknowledgement rule (none, one, all) sets what a crash loses, and an external arbiter decides who takes over.›Double-entry ledgerfinance: Append-only journal of transactions whose postings sum to zero per currency; balances are derived, so money can only move, never appear or vanish.›Event sourcingfinance: Persist state changes as an append-only stream of immutable events per entity; current state is a fold over them, guarded by expected-version appends.›Idempotency keysfinance: Client-chosen unique key per logical operation, stored with a request fingerprint and the final response, so retries replay the first result instead of acting twice.›Iso 20022 messagesfinance: ISO 20022 XML payment and securities messages (pacs, pain, camt, sese): structure and validationMoney representationfinance: Store money as integer minor units with an explicit currency exponent, compute in wider integers, and round once with a named rounding mode.›Reconciliationfinance: Match internal ledger records against bank and processor statements by reference, then amount and date window; every unmatched or mismatched item becomes an exception.›Process supervisionhardware-os: A parent process starts services, reaps their exits and restarts them under a delay and burst-limit policy, then stops them with SIGTERM before SIGKILL.›Webhook deliverynetworking: Reliable server-to-server HTTP callbacks: signed POSTs, 2xx-means-done, retries over days with jitter, endpoint disabling, SSRF guards and idempotent receivers.›Subset sum dpoptimization: Subset-sum / knapsack DP for matching one amount to a combination of manyExternal merge sortsearch-sort: Sort data larger than RAM: write memory-sized sorted runs, then k-way merge them with large sequential buffers; terabytes in one merge pass.›Fuzzy string matchingsearch-sort: Edit distance, Jaro-Winkler and trigram indexes for approximate name and address matchingCoverage-guided fuzzingsecurity: Mutate inputs, run an instrumented target, and keep any input that reaches a new code edge; feedback turns blind guessing into incremental search.›Mutual TLSsecurity: TLS where the client also proves a certificate key, so every connection carries a verified, rotatable machine identity.›Change data capturestorage: Decode the database's own commit log into row-change events in commit order, with a resume position, so any consumer can mirror the data exactly.›Hash joinstorage: Build a hash table on the smaller input's join key and probe it with every row of the larger one; the linear-time workhorse for equi-joins.›Point-in-time recoverystorage: Periodic base backups plus a continuously archived change log let you rebuild the state as of any instant, then branch from there.›Sort-merge joinstorage: Sort both inputs on the key, then advance two cursors in lockstep and emit matching runs; cheap on sorted inputs and good for range joins.›Graceful drainingsystems: On a termination signal, stop taking new work, finish in-flight work until a deadline, tell clients to reconnect elsewhere, then exit; clients resume from a token.›Cron schedulingsystems: Cron expression parsing and next-fire-time computation, with catch-up and time-zone rulesCooperative schedulingconcurrency: Green threads/coroutines yielding at await points on a user-space schedulerFault injectiondistributed: Inject latency, errors, kills and partitions via proxies, netem or hooksRecord replaydistributed: Record every nondeterministic input (time, randomness, I/O, model and tool outputs, per-tick player input) and re-execute deterministically for debugging, replays and state rebuildHealth checkingsystems: Decide per backend whether to route to it using probes and real-traffic errors, with consecutive-count hysteresis, capped ejection and a panic fallback.›Leasesdistributed: A lock that expires on its own: bounded-time ownership that survives crashes and partitions, given bounded clock drift and fencing tokens on writes.›Optimistic concurrency controlstorage: Run transactions without locks, record what they read, and validate at commit that nothing changed; fastest when conflicts are rare.›Cryptographic hashes (SHA-2,BLAKE3)security: Collision-resistant fingerprints of data; SHA-256 for interoperability, BLAKE3 for speed, HMAC/HKDF for keyed uses.›Largest remainder roundingfinance: Splitting an amount into parts that sum exactly (largest remainder / Hamilton) for allocations, splits and prorationsPRNGs (PCG, xoshiro)numerics: Small, fast, statistically strong non-cryptographic generators: PCG32 and xoshiro256**, with seeding, parallel streams and unbiased ranges.›Exponential backoff with jittersystems: Retry after delays that grow geometrically and are randomized, so failed clients spread out instead of hammering a recovering service in synchronized waves.›Write-ahead logstorage: Append each change to a checksummed sequential log and fsync it before touching data pages; the basis of crash durability in nearly every database.›+ 17 moreTransparent interceptionnetworking: Redirect traffic to a local proxy (iptables TPROXY/REDIRECT, eBPF) without app changesSyscall interceptionsecurity: Intercept syscalls via ptrace, seccomp user notify or a user-space kernelFencing tokensdistributed: A number that grows with every lock grant and is checked by the protected resource, so a paused ex-holder's late write is rejected.›CRC32C checksumshardware-os: The Castagnoli CRC-32 with dedicated instructions on x86 (SSE4.2) and ARMv8. It is the standard fast integrity check for storage blocks, logs and network frames.›fsync, O_DSYNC and O_DIRECThardware-os: What fsync, fdatasync, O_DSYNC, O_DIRECT and F_FULLFSYNC really guarantee on Linux and macOS, and the exact call sequences for crash-safe files.›+ 8 moreEbpfhardware-os: Verified in-kernel programs on hooks (kprobes, XDP, tc) with mapsPtrace process controlhardware-os: Ptrace: attach, read and write registers and memory, single-step, intercept signalsSeccomp bpfsecurity: Syscall filtering with seccomp BPF programsSIMD programminghardware-os: Process 4-16 lanes per instruction with AVX2, AVX-512 or NEON via Rust std::arch, with runtime dispatch and a portable fallback.›+ 5 more+ 1 more

How it is built

Architecture overview

A reconciliation system compares two or more sets of records that should describe the same money: the company's ledger, a payment processor's report, the bank statement, sometimes a partner's file. It matches what agrees, explains what does not, and turns every difference into a tracked exception that someone resolves. It runs daily in most companies, intraday in the largest, and its output feeds the books, the close and the audit.

The idea that makes it work is a cascade of passes from strict to loose, with every item ending in exactly one state. Cheap exact joins on a reference clear the bulk of the volume. Windowed and grouped passes handle what the references cannot (payouts that bundle many sales, amounts that settle a day later). Whatever remains is not guessed at but queued, with an age and a reason. The correctness property is a balance check: matched plus unmatched on each side must add up to the statement's opening and closing balances, which catches missing lines that item-by-item matching never sees.

Reconciliation does not fix anything by editing a source. A resolved exception becomes a new ledger adjustment, so both sources stay as they were received and the audit trail is complete.

Parts:

 internal ledger ──CDC──┐
 processor reports ─────┼──► intake (idempotent, by source+id) ─► raw store ─► normalise ─┐
 bank statements ───────┘                                                                 ▼
                                                              pass 1 reference join ─► matched
                                                              pass 2 amount + window ─► matched
                                                              pass 3 grouped sums    ─► matched
                                                                         │ rest
                                                                         ▼
                  ledger adjustments ◄── analyst / rules ◄── exception queue (age, reason, owner)
                         │
                         └─► balance check vs statement opening/closing ─► close report

How the parts fit together

Intake

Every source is ingested idempotently: each external line is keyed by (source, statement id, line number) or the provider's own id, so re-importing a file changes nothing (Idempotency keys). Statement formats differ: ISO 20022 camt.053 (end of day), camt.052 (intraday) and camt.054 (notifications), SWIFT MT940 and, in the US, BAI2 (see the Reconciliation block for the format list and iso-20022-messages, no page yet, for message structure). Raw files are kept unmodified, with a content hash (SHA-2 and BLAKE3), so the parser can be fixed and re-run later.

The internal side comes from the ledger. Rather than ask the ledger database for "what changed since yesterday" with ad-hoc queries, Change data capture decodes its commit log into ordered row-change events with a resume position, and Consumer offsets record how far reconciliation has read. Where a source offers an API, Incremental sync cursors give the same property: pull only what is new, advance the cursor after the data is stored, and use Message sequence numbers to detect gaps. Processors that push events use Webhook delivery semantics: duplicates are normal, so the intake deduplicates by event id.

Normalisation

Matching is only as good as the normalisation. Amounts are parsed straight into integers in minor units, with sign convention fixed (Money representation); currencies are ISO codes; dates become day numbers in one time zone, with the value date kept apart from the booking date; references are trimmed, upper-cased and stripped of known noise. A CRC-32C over the file segment is a cheap integrity check at rest, distinct from the cryptographic hash used as identity.

Matching engine

The passes follow the cascade in the Reconciliation block; this page is about how they scale and combine.

  • Pass 1, reference join. Hash one side by reference and probe with the other: a hash join, linear in the inputs. Where both inputs already arrive sorted by reference (statement files often do) or do not fit in memory, a sort-merge join is cheaper: sort with pdqsort in memory, or External merge sort with a k-way-merge (no page yet) when the data is bigger than RAM. The choice is the classic hash-versus-sort trade-off, made per batch from sizes and sort order.
  • Pass 2, amount and window. Bucket the unmatched by (currency, amount); inside a bucket match each record to the closest unused counterpart within a tolerance of a few days. Ties break deterministically (oldest first), so a rerun gives the same answer. A d-ary heap serves the "closest first" order when buckets are large.
  • Pass 3, grouping. Processor payouts are net of fees and refunds and bundle many sales. Group the internal side by payout or batch id and match the group sum to one bank line. Where no group key exists, finding a subset of items that sums to a bank amount is subset-sum, pseudo-polynomial in the amount with a DP (subset-sum-dp, no page yet) and only worth it for small candidate sets. Union-find tracks the groups as links are discovered (one bank line matched to several items that share a payout).
  • Fuzzy matching. For references that differ in spelling, fuzzy-string-matching (no page yet; edit distance, Jaro-Winkler, trigram indexes) proposes candidates. Fuzzy matches are one-to-one only, scored, and above a threshold go to review, not to auto-accept.

For large volume the inner loops use SIMD programming for key comparison. Matching is a pure function of two normalised sets, which is why it can be tested and replayed.

Ledger and exceptions

Every unmatched or mismatched item enters an exception queue with source, amount, age and a reason code (missing in bank, missing in books, amount differs, duplicate, timing). Analysts or rules resolve them, and the resolution is written to the double-entry ledger as an adjustment (a pair of postings), never as an edit of a source record. The exception log is an event-sourced stream: opened, assigned, commented, resolved. Two analysts working the same item are handled with Optimistic concurrency control: the second save sees the changed version and must review. Fee or interest allocations across entities that must sum exactly use largest-remainder-rounding (no page yet).

Orchestration

A daily run is a batch with many steps: ingest, normalise, match, balance-check, report. batch-checkpoint-restart (no page yet) is the pattern: persist progress after each step and make every step idempotent, so a crash resumes instead of redoing or duplicating. Schedules (end of day per time zone, bank cut-offs, month-end) come from cron-scheduling (no page yet). Ingest and parse tasks run through a Durable job queue with leases, and a file that keeps failing parks in a Dead-letter queue rather than blocking the run. A Circuit breaker protects the pull from a bank or processor API that is down.

Platform

The store is a database with a write-ahead log; the raw files and event streams use a segmented log layout. Failover follows primary-backup replication with leases and fencing tokens so two matching workers cannot both own a run. With point-in-time recovery a bad import can be undone without losing the day. Connections to banks and processors use Mutual TLS. Services run under Process supervision and drain on deploy (Graceful draining).

Because the result is money, it is tested the hard way: Deterministic simulation testing replays runs with duplicated, reordered and late inputs, and parsers are fuzzed (Coverage-guided fuzzing) since they read untrusted partner files.

hdr-histogram (no page yet) records how long a run takes from last input to published exceptions, and how long a single pass takes, as percentiles; a pass that is fast on average but slow in its tail shows up before the cut-off for the next settlement day is at risk.

Data and control flow

Flow 1: daily bank reconciliation.

  1. A schedule fires after the bank's statement time. The run is created with an idempotency key (account, date).
  2. Intake fetches the camt.053, stores the raw file and hash, parses it and inserts lines keyed by statement id and line number. A re-run inserts nothing new.
  3. The internal ledger events for the same value dates are read from the CDC stream.
  4. Matching runs pass 1, 2 and 3. Each pass removes matched items from the pools of the next.
  5. Remaining items become exceptions. The balance check compares opening balance plus matched and unmatched movements to the closing balance; a mismatch fails the run loudly.
  6. The close report is written and the run is marked complete. Time goes to waiting for the bank and to the exception review by people, not to the join.

Flow 2: resolving an exception.

  1. An analyst opens "bank line with no ledger entry". The system proposes candidates (fuzzy match, same amount).
  2. They link it to an existing entry (a timing difference) or book a new adjustment (a fee the books missed).
  3. The adjustment is posted to the ledger with a reference to the exception; the exception event stream records who and why.
  4. The next run sees the new entry and matches normally.

Flow 3: processor payout.

  1. The processor's report lists the payout with its line items; the bank shows one credit.
  2. The report is matched to the internal sales and refunds by id (strict).
  3. The payout sum, net of fees, is matched to the bank credit by amount and date window.
  4. A difference (a fee not in the report) is an exception with the delta.

Scaling

  1. People, not computers, are the first limit: the exception queue grows with volume faster than staff. Lever: raise the auto-match rate with better references and rules, and rank exceptions by amount and age.
  2. Matching cost is dominated by the larger side. Lever: hash join on references for the bulk, sorted inputs for the rest, partition by (account, currency, date) and run partitions in parallel.
  3. Memory is bound by the hash side. Lever: sort-merge or external sort when inputs exceed memory.
  4. Source latency: banks deliver on their own schedule. Lever: intraday reports for large accounts, and tolerance windows that keep timing differences out of the queue.
  5. History: years of lines. Lever: partition by date, keep recent hot, archive older in cheap storage.

Failure modes

FailureSymptomCauseFix
Double importtotals twice the bank statementre-delivered file or event without a stable keykey every line by source id; deduplicate at intake
False matchwrong items linked, differences hiddenloose fuzzy matching or non-deterministic tiesstrict passes first, one-to-one rule, deterministic tie-break, review above a threshold
Sign or currency mix-uplarge, perfectly mirrored exceptionsdifferent sign conventions or minor-unit exponentsnormalise once; reject records without currency
Timing noisemany exceptions that vanish the next dayvalue date versus booking date, weekend and holiday gapsmatch on value date with a window; business-day calendars
Lost linebalance check failsgap in source feedsequence-number or cursor checks, compare to statement balances
Partial runhalf the data reconciled after a crashno checkpoints, non-idempotent stepscheckpoint/restart, run key
Rounding differencesone-cent residualsper-line rounding at the processortolerance rule with a named rounding account
Parser breakzero lines from a known good accountthe bank changed formatschema validation, fuzzed parsers, alert when line count is zero
Edit of historyaudit cannot reproduce a reportadjustments overwrote source rowsadjustments only as new postings; raw files immutable

Real implementations

Dedicated reconciliation products (BlackLine, Trintech, Duco, Modern Treasury) are closed. Open code exists in ledgers that include reconciliation features and in the CDC tooling that feeds them.

namewhat it isrepolicencenotes
Blnkopen-source ledger and financial coreblnkfinance/blnkApache-2.0its README lists reconciling internal records with external statements
Formance Ledgerprogrammable open-source core ledgerformancehq/ledger—ledger side of the adjustment flow; licence not reported by the GitHub API
Debeziumchange data capture for several databasesdebezium/debeziumApache-2.0the intake of internal changes
ERPNextopen-source ERP with a bank reconciliation toolfrappe/erpnextGPL-3.0has a bank_reconciliation_tool doctype under accounts (seen in the repo tree)

Sources

  • H. W. Kuhn, The Hungarian method for the assignment problem, Naval Research Logistics Quarterly 1955. The classic algorithm for optimal one-to-one assignment when matching candidates carry scores, the alternative to greedy tie-breaking. DOI
  • Stripe documentation on reconciling payouts with the underlying transactions. docs
  • Debezium documentation on change data capture. docs
  • ISO 20022 message families camt and pain (iso20022.org).
  • Repositories listed above.

Similar systems

Share the most building blocks with this one.