Skip to content
Tanner Young
All projects

Ledger Sync

A reconciliation service that matches transactions across three accounting systems without a nightly batch window.

TypeScriptPostgreSQLDistributed Systems

Placeholder case study. Sample content demonstrating the problem / approach / outcome structure. Replace with a real project — the frontmatter above drives the card on the home page and the projects listing.

Problem

Three systems held overlapping records of the same transactions: a payments processor, an internal ledger, and the general ledger in the accounting platform. Reconciliation ran as a nightly batch job that took four hours and produced a spreadsheet of mismatches for someone to work through by hand.

The batch window was the real constraint. Discrepancies were discovered up to a full day after they occurred, which meant the investigation always started with reconstructing what the state had been yesterday. Roughly 60% of the resolution time went into that reconstruction rather than the fix.

Approach

The core insight was that reconciliation did not need to be a batch operation. Each system already emitted events; the batch job existed because nobody had built a place to put them.

Event ingestion. Each source writes into an append-only table keyed by a natural transaction identifier, with the source system and its own sequence number preserved. Nothing is mutated after write, so the history stays auditable.

Incremental matching. A matcher process advances through unprocessed events and groups them into reconciliation units. Most match immediately; the interesting cases are the ones that do not.

-- Transactions seen by at least one system but not yet by all three,
-- older than the expected settlement lag.
SELECT transaction_key,
       array_agg(DISTINCT source_system) AS seen_in,
       min(observed_at)                  AS first_seen
FROM ledger_events
GROUP BY transaction_key
HAVING count(DISTINCT source_system) < 3
   AND min(observed_at) < now() - interval '15 minutes';

Tolerances as data. Matching rules — rounding tolerance, expected settlement lag per source, currency handling — live in a configuration table rather than in code. Finance adjusts them without a deploy, which removed the engineering team from the loop entirely on the most common class of change.

Explicit unresolved state. Rather than mismatches accumulating silently, an unmatched unit becomes a first-class record with an owner and a status. The queue is the interface.

Outcome

  • Detection latency went from up to 24 hours to under 15 minutes.
  • The nightly batch job was removed, freeing the four-hour window.
  • Manual investigation time dropped by roughly 70%, mostly by eliminating the state-reconstruction step.
  • Tolerance changes moved from a multi-day engineering request to a self-service edit.

The unexpected result was cultural: when mismatches surfaced within minutes, they were investigated while the context was still fresh, and the underlying causes actually got fixed. Under the batch regime they had been treated as background noise to be cleared.