Why We Build Reconciliation Into the Architecture — Not Bolt It On Later

Why We Build Reconciliation Into the Architecture — Not Bolt It On Later

29.05.2026

Here's how it usually goes. Sprint 1: build the payment engine. Sprint 2: add the merchant portal. Sprint 3: launch. Sprint 6: "wait, how do we know the bank actually received these payments?" Sprint 8: someone builds a reconciliation script. Sprint 14: the script has 47 hardcoded exceptions and nobody wants to touch it.

We've inherited that script. More than once. The script is never the problem — it's a symptom. The problem is that reconciliation was treated as something you add to a payment system, like adding analytics to a website. It's not. Reconciliation is the mechanism by which your system knows whether it's telling the truth.

A payment system without reconciliation built into its architecture is a system that believes its own database. And databases don't talk to banks.

The two versions of reality

Every payment system contains two versions of every transaction. There's your version — what your system recorded when it sent the instruction. And there's the bank's version — what the bank recorded when it processed (or didn't process) the instruction.

These two versions should match. They frequently don't.

Your system says you sent €45,000 to Merchant X at 2:15 PM. The bank statement shows €44,850 debited at 2:17 PM. Where's the €150? Bank fee. Your system didn't know about it because the fee structure changed last month and nobody updated the configuration.

Your system says you sent five payments in a batch. The bank settled four. The fifth was returned with a reference code that your system doesn't recognise because it's a new return reason the bank introduced last quarter.

Your system says you have €3.2M in Account 4. The bank says €2.9M. The difference is three payments that your system marked as "sent" but the bank hasn't settled yet — they're sitting in a holding state because the bank's batch processor runs at 6 PM, not immediately.

None of these are bugs. They're the normal, daily reality of operating a payment system. And they're invisible unless your architecture is designed to surface them.

What "built in" means, concretely

When we say reconciliation is part of the architecture, we mean specific design decisions made before the first line of payment code is written.

Every transaction carries a reconciliation lifecycle

A transaction in our systems doesn't just have a payment status (pending, sent, completed, failed). It has a reconciliation status: unmatched, matched, partial_match, exception, resolved.

A payment marked "completed" with a reconciliation status of "unmatched" is a payment your system thinks succeeded. Maybe it did. Maybe it didn't. You won't know until the bank confirms it — and that confirmation comes through the reconciliation process, not through the payment API.

This dual-status model means the operations team can see, at any moment, not just "which payments went out" but "which payments have been confirmed by the bank." The gap between those two numbers is their exposure.

The bank's data is a first-class entity

Most payment systems store the bank statement as a file — something that gets downloaded, parsed, and compared against. The statement data itself lives in a temporary staging area, or worse, in memory during the matching process.

In our architecture, bank statement data is a permanent, queryable entity. Every line item from every statement is stored, linked to the original file, and maintained indefinitely. It's the other half of the reconciliation pair.

This means you can answer questions like:

  • "Show me every bank statement entry that has never been matched to an internal transaction" — these are transactions the bank knows about that your system doesn't
  • "Show me every internal transaction from last month that was never confirmed by a bank statement" — these are transactions your system thinks happened but the bank doesn't
  • "Show me the history of all bank statement entries for this merchant" — useful for dispute resolution, without digging through files

Matching rules are configuration, not code

The matching logic — how a bank statement entry gets linked to an internal transaction — is not buried in application code. It's expressed as configurable rules with explicit criteria, confidence scores, and escalation paths.

This matters because matching rules change. A new bank has different reference conventions. A new merchant type introduces split payments. A regulatory change requires a different fee treatment. If every matching rule change requires a code deployment, your reconciliation is coupled to your release cycle. If matching rules are configuration, the operations team can adapt without waiting for engineering.

Exceptions are a workflow, not an error state

In a bolt-on reconciliation, an unmatched transaction is a problem. It shows up in a report, someone investigates it, and the resolution happens outside the system — in email, in a spreadsheet, in someone's head.

In our architecture, an unmatched transaction enters a workflow. It's assigned to an operator. The operator sees the internal record and the bank statement entry side by side. They can approve a match (with a reason), create an adjustment, flag it for escalation, or mark it as a known timing difference that will auto-resolve tomorrow.

Every resolution is recorded: who, when, what they decided, and why. Not because we're compliance-obsessed (though it helps) — because when someone asks "why was this €500K transaction manually matched?" in three months, the answer should be in the system, not in someone's memory.

The nine-step descent (how bolt-on recon evolves)

We've seen this exact progression at multiple companies. It's so predictable we've numbered the steps:

  1. Payment system launches. No reconciliation. "We'll check the bank manually."
  2. Someone writes a script that compares the database to the bank statement CSV.
  3. The script becomes a cron job. It emails a spreadsheet every morning.
  4. The spreadsheet gets a conditional formatting rule to highlight mismatches. Nobody calls it a "dashboard" but that's what it is.
  5. Edge cases emerge. The script gets if statements. "If the reference came from Bank X, trim to 16 characters before matching."
  6. More edge cases. The if statements get their own config file. Someone calls it "the rules engine" but it's a JSON file with 200 lines of special cases.
  7. The script breaks whenever the payment system's schema changes. Nobody told the recon person about the migration. Two days of unreconciled transactions pile up.
  8. Management asks for a "proper reconciliation system." An architect estimates four months. Management says "can't we just improve the script?"
  9. The architect explains that improving the script requires refactoring how transactions are stored, how references are generated, and how bank data is ingested. It's not a script problem — it's a data model problem.

Step 9 is where it gets expensive. Not four months — more like eight, because now you're changing the foundation of a running system while keeping it operational.

We build for step 0: reconciliation as a design constraint before the first transaction is processed. It costs more in sprint 1. It costs dramatically less in total.

A practical starting point

If you're designing a payment system today and reconciliation isn't in the architecture yet, start here:

Add recon_status to your transaction model. Four values: unmatched, matched, exception, resolved. Default to unmatched. Change it only through the reconciliation process, not through the payment flow.

Build your bank statement ingestion before your dashboard. You can't reconcile what you haven't ingested.

Write your matching rules in plain language first. "Same reference, same amount, same day = auto-match. Same reference, different amount by less than €50 = probable fee, flag for review." Then implement them.

Build the exception queue before you need it. By the time you need it, you're already behind.


Zenlime builds payment systems where reconciliation is architecture, not afterthought. Let's talk.