A case study
Ledgerline 2025
Turning a three-day spreadsheet ritual into a forty-second stream.
- In one line
- Real-time payment reconciliation engine
- My role
- Lead engineer — architecture, streaming pipeline, on-call runbook
- Timeline
- 8 months · 2024–2025
- Team
- 4 engineers, 1 product manager, embedded finance analyst
- Built with
- Go, PostgreSQL, Kafka, Kubernetes
Ledgerline replaced a manual reconciliation process — analysts diffing bank statements against an internal ledger in spreadsheets — with a streaming engine that matches every transaction as it lands. The goal was not just speed but auditability: every match, and every exception, had to be explainable to a regulator.
Where it started
The situation
Thirty thousand transactions a day were reconciled by hand each morning. A discrepancy from Monday might not surface until Wednesday, by which point the money had already settled and the fix meant a clawback. The process didn't scale, and it left no trustworthy audit trail.
The constraint
This was money. A reconciliation engine that is fast but occasionally wrong is worse than a slow one that is always right. Correctness and a complete, immutable audit log were hard requirements — throughput was the negotiable part.
How it was built
Idempotent ingest
Bank events and ledger events flow into Kafka topics keyed by a deterministic transaction fingerprint. Re-delivered events collapse onto the same key, so the pipeline is safe to replay end-to-end — essential for both recovery and backfilling history.
A matching state machine
Each transaction moves through an explicit state machine (unmatched → provisionally matched → confirmed → exception). Every transition is written to an append-only table in PostgreSQL with the rule that fired it, so any analyst can reconstruct exactly why two records were paired.
Exceptions as first-class work
Unmatched transactions after a tunable window are routed to an exceptions queue with the candidate matches ranked. Analysts stopped hunting for problems and started resolving a prioritized list — the human effort moved to where judgment was actually needed.
Observability from day one
Match latency, exception rate, and queue depth are exported as first-class metrics with alerts tied to the reconciliation SLO. The on-call runbook was written alongside the code, not after the first incident.
What it changed
- Reconciliation lag
- 3d → 40s
- Unexplained variances, year one
- 0
- Transactions matched automatically
- 30k/day
- Auto-match rate
- 98.7%
Looking back
The state machine was the decision that aged best — making every transition explicit turned 'why did this match?' from an investigation into a database query. If I did it again I'd invest earlier in a replay harness for historical data; backfilling a year of transactions taught us where the model's edges were, and I'd rather have learned that in week two than month six.