Skip to content
Engineering9 min read

Architecting a Multi-Rail Settlement and Reconciliation Engine

A reference architecture for reconciling ledger intent against what payment rails actually confirm, across ACH, RTGS, cards, and correspondent banking.

  • Event sourcing
  • Double-entry ledger
  • Kafka
  • ISO 20022
  • Idempotency keys
  • Outbox pattern
  • PostgreSQL
  • Saga orchestration

By TRAGenX Engineering

The problem: rails don't agree on what "settled" means

A payments platform rarely moves money over one rail. A single operator might push ACH debits, receive RTP or FedNow credits, settle card network interchange in net batches, and wire correspondent-bank transfers for cross-border legs — often within the same product. Each rail has a different timing model: some are real-time gross settlement, confirming (or failing) a transfer in seconds; others are deferred net settlement, where your "confirmed" position today is actually a projection that gets trued up against a batch file tomorrow. Some rails push a webhook the moment something happens; others only tell you the truth once a day, in a flat file dropped on an SFTP server at a cutoff time that varies by bank holiday calendar.

The engineering problem is not moving money — that's a thin adapter per rail. The problem is that your internal ledger records an intent to move money, and each rail's own record of what happened is the only source of truth for whether that intent became a fact. Reconciliation is the process of proving, continuously, that your internal belief matches external reality, and settlement is what happens in the (unavoidable) window where it doesn't yet. Any architecture that treats a payment instruction and a settlement fact as the same event will eventually book money that was never actually received, or fail to notice money that silently didn't move.

Core data model: separate intent, fact, and match

The center of the system is an append-only event ledger with three distinct record types, never conflated. A payment instruction (intent) captures what we asked a rail to do — amount, currency, counterparty, rail, and a client-generated idempotency key. A settlement event (fact) captures what a rail told us actually happened, sourced only from rail-authoritative channels: a webhook, a camt.053 or MT940 statement, a NACHA return file, a card network settlement report. A reconciliation record links a fact to an intent, or explicitly records that no match was found.

Each rail gets a canonical translation layer rather than being modeled as a special case throughout the codebase: an internal transaction schema (rail-agnostic — currency, amount, parties, purpose code, timestamps) with adapters that map to and from each rail's native format, ISO 20022 pain.001/pacs.008 messages for higher-value or cross-border rails, NACHA for ACH, proprietary JSON for card processors. Underneath the transaction layer sits a conventional double-entry ledger: every settlement fact posts a balanced entry against a rail-specific clearing account and the customer or internal position account. The clearing account balance for a given rail is, by construction, the running total of money we believe is in flight but not yet reconciled — a useful invariant, since a clearing account that doesn't trend toward zero as statements arrive is the first sign something is actually broken, not just late.

Write path and read path: submission is a saga, reconciliation is a diff

On the write side, a payment instruction goes through an outbox pattern: the instruction and its ledger-pending entry are written in the same local transaction, and a separate dispatcher reads the outbox and submits to the rail adapter, tagging every submission with an idempotency key derived from the instruction, not generated fresh on retry. This matters because most rails offer only at-least-once delivery on their side — a network timeout after submission but before acknowledgment is genuinely ambiguous, and the only safe response is to retry with the same key and let the rail (or your own dedup layer) reject the duplicate, rather than guess whether the first attempt succeeded.

The read path is structurally different: it's a periodic or event-driven ingestion of the rail's own record — a statement file, a settlement report, a webhook payload — normalized into settlement-fact events and appended to the same event log. A separate reconciliation process then runs what is fundamentally a three-way diff: expected settlement facts (derived from pending instructions), actual settlement facts (from the rail), and existing ledger postings. Matches close automatically. Everything else — an instruction with no matching fact past its expected window, a fact with no matching instruction, an amount mismatch — becomes an exception record, because auto-resolving ambiguous money movement is a worse failure mode than a slower manual review.

Consistency and failure handling: unmatched is a state, not an error

Because no rail in this design offers exactly-once settlement confirmation, the system has to treat "unmatched" as a normal, expected, time-bound state rather than an error condition. Each rail gets a configured reconciliation window — informed by that rail's actual settlement cycle, not a single global timeout — after which an unmatched instruction transitions from pending to aged, and after which it transitions again to an exception requiring a human decision: resubmit, mark failed and reverse the pending ledger entry, or escalate to the counterparty bank. This state machine, not a boolean success/failure flag, is what makes the system auditable: every settlement fact traces back through an explicit sequence of states, and nothing gets silently written off.

Partial rail outages are handled by isolating adapters so that one rail's degraded state doesn't block others — a stalled SFTP drop from one correspondent bank should never stall ACH processing. The harder failure mode is a rail that's up but lying — sending confirmations for instructions it never actually executed, or a statement file that's truncated rather than absent. Guarding against that means never trusting a rail's confirmation in isolation; the reconciliation record only closes when the statement-level fact and the confirmation-level fact agree, and a mismatch between the two is itself an exception, not an averaged-out success.

Scaling, operability, and the trade-offs we'd actually make

Operationally, the natural shape is an event-driven pipeline behind a durable, ordered log — Kafka or an equivalent — so that settlement facts and reconciliation runs are replayable. This matters more than raw throughput: a bug in matching logic discovered after the fact needs to be fixable by replaying history through corrected code, not by re-pulling statements from banks that may not keep them available indefinitely. Reconciliation itself should be an idempotent batch or streaming job that can rerun safely against the same window without double-posting, since "we reran reconciliation and it changed the ledger" is an incident, not a feature.

The trade-off we'd actively make is against full automation of exception resolution. It's tempting to expand matching rules until the exception queue approaches zero — fuzzy-matching on amount and near-timestamp, say — but every rule that auto-resolves an ambiguous case is a rule that can auto-resolve the wrong case, silently, at the exact moment something is actually wrong (a duplicate submission, a rail misrouting funds). We'd rather keep a bounded, well-instrumented human-review queue — tracking aging and match-rate trends as operational signals, not as claimed performance numbers — than optimize away the one place where judgment catches what pattern-matching won't. Similarly, we'd choose per-rail adapter isolation over a unified "smart" ingestion service even though it duplicates some plumbing, because the failure blast radius of one bad rail integration staying contained is worth more than the code reuse.

Building something like this?

This is the kind of foundational systems work TRAGenX takes on. If you have a project that needs a core other things can trust, tell us about it and we'll get back to you.