◐ ai-teacher

🛡 Quality, Testing & Ops · Chapter 5

Reconciliation: proving two systems agree

Two systems that should agree never quite do. Reconciliation is the discipline of explaining every rupee of the difference — and the explained part is the bit people forget.

Study this lesson free in the app →

The basics

Reconciliation is proving that two systems hold the same facts — that the warehouse matches the source database, that rides match payments, that payments match what the bank actually settled — and explaining every difference.

Why it exists: data is copied, transformed and re-derived many times between where it's created and where it's reported. Each hop can drop, duplicate or alter rows. Tests on one table can't see that; only comparing two tables can. For anything involving money, auditors and finance teams will ask you to prove the numbers agree.

The mental model — zoom levels:

  1. Counts — 95,214 rides in source, 95,214 in warehouse?
  2. Control totals — sum of fares equal?
  3. Key-level diff — which ride_ids are in one side only?
  4. Row/column diff — for matching keys, which values differ?

Start coarse and cheap, zoom in only when a level disagrees.

The other half: a difference isn't automatically a bug. Rides at 23:58 paid at 00:01, payments still pending, refunds, rounding — these are expected breaks that you classify and explain. What's left after that is the real problem.

Words to know: control total, break, tolerance, cut-off, anti-join.

Explained

Meghalaya Cabs, 4 November.

Three systems, three numbers, all ‘correct’. The CFO wants to know which one is revenue and why the others differ. Reconciliation is the answer to that question — line by line.

Closing the shop till

At 10 PM the shop owner in Police Bazaar counts the till: ₹41,300. The billing machine says ₹43,150 in sales.

She doesn't panic. She explains:

₹1,850 difference, ₹1,800 explained. The ₹50 is the only number that needs investigating — and she can only find it because she explained the rest first.

That's reconciliation: categorise every difference until what remains is small enough to chase.

Zoom in only when you have to

Counts can match while totals don't (a fare changed); totals can match while keys don't (two errors cancelling). That's why serious recon goes at least to level 3 for money.

The key-level diff

WITH r AS (
  SELECT ride_id, fare_inr FROM fct_rides
  WHERE status = 'completed'
    AND completed_at >= '2026-11-04 00:00 +05:30'
    AND completed_at <  '2026-11-05 00:00 +05:30'
), p AS (
  SELECT ride_id, sum(amount_inr) AS paid_inr     -- a ride may have 2 attempts
  FROM gateway_payments
  WHERE state = 'captured'
    AND captured_at >= '2026-11-04 00:00 +05:30'
    AND captured_at <  '2026-11-05 00:30 +05:30'  -- 30-min grace for late capture
  GROUP BY ride_id
)
SELECT coalesce(r.ride_id, p.ride_id) AS ride_id,
       r.fare_inr, p.paid_inr,
       CASE
         WHEN p.ride_id IS NULL THEN 'missing_in_gateway'
         WHEN r.ride_id IS NULL THEN 'missing_in_rides'
         WHEN abs(r.fare_inr - p.paid_inr) > 1 THEN 'amount_mismatch'
         ELSE 'matched'
       END AS recon_status
FROM r FULL OUTER JOIN p ON r.ride_id = p.ride_id;

Note the explicit timezone, the grace window, and aggregating payments before joining so retries don't fan out.

Explaining the gap

Rides, completed fares (4 Nov)            ₹38,42,610
  − cash rides (not in gateway)             −14,200
  − captured after cut-off (paid 5 Nov)      −6,950
  − payment pending / failed, retry due      −1,580
= Expected in gateway                      ₹38,19,880   ✓ matches gateway
  − gateway fees + GST on fees              −52,440
  − refunds netted in settlement             −6,200
= Expected bank credit                     ₹37,61,240   ✓ matches bank
Unexplained                                       ₹0

This is the bridge (or walk) finance actually wants: start at one system's number, end at the other's, every step a named reason with its own query.

At scale: hash, then drill

Reconciling 2 billion rows after a warehouse migration with a full outer join is expensive. Instead:

  1. Per partition (say per day), compute count(*) and an order-independent fingerprint, e.g. sum(hash(ride_id || fare_inr || status)) — or bit_xor of row hashes — on both systems.
  2. Compare the 1,000 day-level fingerprints. Typically 995 match.
  3. Run the full key-level diff only on the 5 that don't.

Watch out: both systems must normalise identically before hashing — same decimal scale (245.5 vs 245.50), same timestamp format and timezone, same null representation, same string trimming. Otherwise every partition ‘differs’ and the method is useless.

Tools like data-diff-style utilities and warehouse-native checksums do exactly this bisection.

Migration weekend

Meghalaya Cabs is moving rides from Postgres to Snowflake. Cut-over is Sunday.

Saturday: counts match for all 730 days. Totals match for 727. Three days differ by exactly ₹0.01 × thousands of rows.

Cause: Postgres numeric(10,2) vs a float column in the new staging layer — rounding drift on fares like ₹245.15. Not a lost ride, but finance would have found it in the first month's close.

Fix the type to number(12,2), rerun, all 730 days match on hashes. That's the sign-off artifact: a table of 730 rows, each with count, total and hash from both sides, all green.

Gotchas

Words to know

ReconciliationSystematically comparing two systems that should agree and accounting for every difference.
Control totalAn aggregate (count, sum) computed on both sides as a cheap first comparison.
BreakA record or amount that doesn't match between systems. Classified as expected (timing, fees) or unexplained.
Cut-offThe exact time boundary, with timezone, that decides which day a record belongs to on each side.
Anti-joinSelecting rows in one table with no match in the other — the core of finding missing records.
Bridge (walk)A step-by-step explanation from one system's total to another's, each step a named reason.
Three-way reconciliationMatching across three systems — e.g. rides, payment gateway, bank settlement.

In two minutes

Test yourself

1. What is the right order of zoom levels for reconciling two tables?

  1. Column diff → key diff → totals → counts
  2. Counts → control totals → key-level diff → column diff
  3. Hashes only
  4. Key-level diff on every row first
Show the answer

Counts → control totals → key-level diff → column diff — Start with cheap, coarse checks and only pay for detail where they disagree. Most partitions match at the count/total level.

2. Which SQL construct classifies every record as matched, missing on one side, or missing on the other?

  1. INNER JOIN
  2. LEFT JOIN
  3. FULL OUTER JOIN on the business key
  4. UNION ALL
Show the answer

FULL OUTER JOIN on the business key — Only a full outer join keeps unmatched rows from both sides. An inner join hides all the breaks; a left join hides one direction.

3. Rides are filtered by IST day, payments by UTC day. What symptom appears?

  1. No visible difference
  2. Fare values change
  3. Duplicate ride_ids
  4. Every day shows a break of roughly the rides in a 5.5-hour window, offset the next day
Show the answer

Every day shows a break of roughly the rides in a 5.5-hour window, offset the next day — The 00:00–05:30 IST window falls on different days in each system, producing systematic daily breaks that roughly cancel across days.

4. A ride had a failed payment attempt and a captured one. You LEFT JOIN rides to payment rows and sum. What goes wrong?

  1. The ride appears twice, doubling its fare in the rides total
  2. Nothing
  3. The ride disappears
  4. The query fails
Show the answer

The ride appears twice, doubling its fare in the rides total — Join fan-out. Filter to captured and aggregate payments per ride_id before joining, so each side is one row per key.

5. Reconciling 2 billion rows after a migration — most efficient approach?

  1. Full outer join on everything
  2. Sample 1,000 random rows
  3. Compare per-partition counts and hashes, then diff only mismatched partitions
  4. Compare the final dashboard numbers
Show the answer

Compare per-partition counts and hashes, then diff only mismatched partitions — Hash-and-drill finds every difference while doing detailed work on only the few partitions that differ. Sampling can miss rare breaks.

6. Trap: daily counts and fare totals match exactly between source and warehouse. Proven identical?

  1. Yes — counts and sums both match
  2. No — a missing ride and a duplicated ride of the same fare cancel out
  3. Yes, if the table has a primary key
  4. No — totals never match exactly
Show the answer

No — a missing ride and a duplicated ride of the same fare cancel out — Offsetting errors are invisible to aggregates. Key-level comparison (or per-row hashes) is the only proof.

There are 8 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →