hamsa software
← All articles
Engineering7 min read

One report can expose the real migration

An old report and its replacement both show £200. Comparing six underlying records reveals two differences that the matching total has concealed.

Two rectangular shapes are joined by six short stitches, suggesting the underlying records that must be compared when report totals agree.

The number agrees a little too easily

Both reports show £200. That would be a reassuring first result in a migration, until someone opens the rows. The old report includes a cancelled £40 order and leaves out a confirmed £40 order that the replacement includes. The mistakes cancel in the footer while changing which orders the report says qualify. It is worth looking at that agreement before treating it as the new system’s acceptance test.

One read-only report can give a team room to examine this without replacing order entry and payments at the same time. The people who use it can point to individual records and explain what they mean. That conversation often reaches details the architecture diagram does not answer: which date counts, what confirmed means, and why a cancelled order appears. A small migration boundary can expose quite a large part of the business rule.

Martin Fowler’s Strangler Fig account describes gradual replacement while old and new capabilities coexist. [1] A read-only report can be one practical boundary for that approach. It is not automatically the right first step for every system, but it can provide a working place to learn what replacement actually means before committing the business to a much larger move.

Ask what decision the report supports

For this original synthetic example, readers agree a specific definition: confirmed orders dated 20 June 2025, with cancelled and pending orders excluded. Amounts use GBP and the fixture stores integer pence. We are not claiming a universal revenue or accounting rule. The definition simply gives this comparison an explicit answer against which the rows can be examined.

That agreement is necessary because an operational workload report might legitimately include an order later cancelled. A sales-planning report might use confirmation date rather than received date. The same word order can name different events in different conversations. Copying an old query without asking its readers can preserve an unintended rule; removing an odd-looking clause without asking can destroy a useful one.

Start by reading one real row with the people who use the output. Which decision does it support? Why is its date the relevant date? Where does its amount come from? Keep the person maintaining the export in that discussion. They may know why a formula differs across sheets, even if the explanation has never been recorded as a requirement.

Write down some orders that belong and some that do not, including the awkward ones. That gives readers something firmer to discuss than “match the old report”. It also allows them to decide that part of the old behaviour was a defect. A replacement need not preserve a cancelled order forever merely because the existing query has become very dependable at including it. Compatibility needs an agreed meaning, as well as matching output.

Compare the rows before admiring the total

Our frozen dataset contains six orders. O1 is confirmed on the chosen date for £100; O2 is cancelled for £40; O3 is confirmed for £40; O4 is confirmed for £60. O5 falls on the following day for £30, and O6 is still pending for £50. These distinctions are visible facts in the teaching data, not changes inferred from the two summaries.

The old output includes O1, O2 and O4. The candidate includes O1, O3 and O4. Each totals £200, but the record comparison has two findings: O2 is extra in the old output under the agreed rule, and O3 is missing from it. The candidate’s exclusions of O5 and O6 are equally important: a replacement that includes every source record could fix the missing order while introducing two new errors.

Compare the union of record identities and classify each difference. Missing on one side is different from present on both with a different amount or state. PostgreSQL documents outer joins that retain unmatched rows. [2] A FULL OUTER JOIN is one way to express this comparison; using only an inner join would remove precisely the missing records you need to investigate.

There is another way for apparently sensible rows to produce the wrong answer: a join can multiply an order with several line items or status-history records. A final DISTINCT may make the output look less crowded without repairing the amounts. Before investigating the discrepancies, establish whether each input represents an order, a line or a status observation. The comparison needs one row per agreed business identity, or it may be measuring its own joining mistake.

The totals agree because two differences cancel out

An original frozen six-order fixture defines the report as confirmed orders on 20 June 2025. Old and candidate outputs both total £200, but contain different records.

Scroll sideways to see all columns.

OrderFrozen source factOld reportCandidate / agreed rule
O1Confirmed 20 June · £100Included £100Included £100
O2Cancelled 20 June · £40Included £40Excluded: cancelled
O3Confirmed 20 June · £40MissingIncluded £40
O4Confirmed 20 June · £60Included £60Included £60
O5Confirmed 21 June · £30ExcludedExcluded: outside period
O6Pending 20 June · £50ExcludedExcluded: not confirmed
TotalSame source snapshot£200£200; two record discrepancies
Synthetic records verified by the downloadable fixture. The declared teaching rule excludes cancelled and pending orders; a real report’s meaning must be agreed with its readers.

Freeze enough context for the difference to mean something

The fixture compares outputs from one frozen dataset. With live data, the old report might read an order before cancellation while the candidate reads it afterwards. That difference concerns observation time rather than necessarily indicating a broken rule. Record the extraction cutoff and relevant versions, and agree how late updates are handled. The comparison then has enough context to distinguish timing effects from persistent differences in interpretation.

PostgreSQL’s isolation documentation explains how concurrent changes become visible under different transaction levels. [3] An appropriate local snapshot can make a database comparison more coherent, but it does not freeze an external provider or an independently updated store. Where inputs cross those boundaries, define the comparison window and the evidence required before treating a mismatch as persistent.

Do not solve every timing mismatch by ignoring recent records indefinitely. Readers still need the report on time. Decide whether the report is a point-in-time view, a revisionable period record or a provisional view with a later finalisation. Those are different products. The replacement should make that behaviour clear rather than inherit a cutoff simply because it happened to be convenient for the data pipeline.

A report difference can be a timing difference

The synthetic six-order comparison freezes its input; live comparisons need a declared observation rule.

Scroll sideways to see all columns.

Observation designWhat a mismatch might meanEvidence required
Old reads before cancellation; new reads afterDifferent observation timesCutoff and relevant record versions
One database snapshotComparable local state under chosen isolationTransaction and isolation semantics
External provider plus local databaseNo shared atomic snapshot is impliedExtraction windows and settling policy
Revisionable period reportLater changes may legitimately alter the resultExplicit provisional/finalisation behaviour
Ignoring recent records indefinitely is not a substitute for deciding what the report promises its readers.

Let explained differences earn the switch

A discrepancy ledger needs an owner and a disposition: candidate defect, old defect, legitimate timing difference, policy question or source-data issue. Attach the supporting record and the agreed interpretation. A long list of unexplained differences is evidence that the replacement is not ready; a small total difference is not evidence that the remaining records are harmless.

The downloadable fixture reproduces the six source records and both outputs. Change O2 from cancelled to confirmed and follow the consequence in the discrepancy set as well as the total. Under the declared rule, it would now belong in the report; a comparison that continued calling it extra would be preserving its old answer rather than applying the policy. The self-comparison provides another useful control: identical outputs should leave no unexplained difference for the migration owner.

For a real shadow period, agree switching conditions with readers. They might require every consequential difference to have a recorded explanation and the candidate to arrive before the planning meeting across representative runs. Include a difficult period, such as a late cancellation or a relevant boundary date, if that is part of the report’s actual job. Repeating the easiest day several times does not establish the behaviour the business depends on.

The overlap needs an ending as well as an old route to fall back to. Running two reports permanently gives somebody a new weekly job: deciding which to trust when they differ. Agree how long the comparison is needed and what would justify switching. That lets the explanations accumulated during shadowing transfer responsibility to the replacement, rather than becoming a growing file of reasons nobody quite wants to choose.

Retire the explanation people no longer need

After the switch, remove the old scheduled export, obsolete access route and instructions when it is safe to do so. Check for readers who consume the file indirectly through a spreadsheet or another report. A retired screen does not necessarily mean the capability has no remaining consumer. Let those dependencies become part of the cutover plan rather than surprises discovered the following Friday.

Keep the agreed examples and the important discrepancy decisions with the new report. They are a compact explanation of its meaning. A future engineer should be able to see why cancelled orders are excluded here, and why another operational report might quite properly include them. That context can prevent the same debate from returning under the heading of another tidy-up.

Some systems will resist even this small slice because the calculation depends on a shared transaction or an undocumented side effect. Discovering that through a report still makes the next investment easier to explain. Here, O2 and O3 give us a more immediate answer: the old and new totals agree because the report includes the wrong £40 order. The migration decision needs to follow the agreed status rule and the records it selects.

Working on a workflow like this? Hamsa helps finance and operations teams connect their tools and handle the exceptions, starting with one workflow.

See the Payment Operations Pilot

References & further reading

Primary sources behind this article. Living documentation may change.

  1. Martin Fowler: Strangler Fig gradual replacement Published 22 August 2024
  2. PostgreSQL: outer joins and unmatched rows
  3. PostgreSQL: transaction isolation and snapshots
Hamsa Jama

Practical engineering. A human perspective.

More about Hamsa Software and the person behind it. Editorial notes.

A few more things
worth a read.

Back to the reading room
Engineering7 min read

What does £240 belong to?

A readable table preserves relationships between values, row labels, column headings and actions across different ways of reading.

Engineering7 min read

When two calculations agree on the wrong answer

A shadow comparison exposes differences. Independent rule examples explain whether those differences are bugs, corrections or changed assumptions.