Topics / Modernization

How to verify a rewrite preserves business behavior

Agree what must remain true, run both implementations against the same controlled cases, and check their results against those requirements. Include stored data and external effects. Two implementations can agree on the same bug.

Define what the rewrite has to preserve

Start with one boundary: an endpoint, a worker or a state transition that someone depends on. Name its consumers, supported input range, required effects and failure behavior. Keep references to the contracts, incidents and decisions that justify each promise.

Existing code and tests help recover candidate requirements. They also preserve historical workarounds and defects. If the sources disagree, record the conflict and its owner before choosing an expected result. The requirements-recovery worksheet provides that starting record.

Write intended changes separately. A replacement might deliberately reject a formerly accepted input or remove a deprecated feature. That needs an explicit compatibility decision, rather than a comparator quietly ignoring the difference.

A successful translation can change an account ID

Illustrative migration scenario. An API supplies decimal account IDs as strings. A Python service converts them to integers. Its JavaScript replacement converts them to Number. Small IDs survive the change; larger ones can lose precision.

# Python 3
print(int("9007199254740993"))
# 9007199254740993

// JavaScript, run with Node.js
console.log(Number("9007199254740993"));
// 9007199254740992

These expressions demonstrate a language boundary, not a deployed customer incident. Python integers have arbitrary precision; JavaScript Number cannot safely represent every integer above 9,007,199,254,740,991.

The requirement should state that distinct supported account IDs remain distinct through parsing, lookup and serialization. Retaining the ID as a validated string may satisfy that requirement. Using BigInt requires an explicit serialization contract. Passing through Number first has already lost the information.

Test the actual endpoint and stored identity at the boundary values, including adjacent IDs. A unit test of the conversion alone cannot show that database adapters or response encoding preserve the fix.

Compare effects, not just response bodies

BoundaryWhat to observe
API contractResponse values, errors, identity, ordering and behavior of supported clients.
Stored stateRows actually committed, ownership, transaction boundaries and state after a rejected call.
External effectsMessages, notifications or charges, including their count and behavior on retry.
Failure recoveryTimeouts, partial completion, restart and allowed duplicate handling.
Operating constraintsRequired latency, concurrency and capacity under the agreed workload.

Use equivalent starting state and identify both revisions, runtime versions and dependencies. Keep the raw observations alongside any normalized comparison. Timestamps or generated request IDs may legitimately differ, but removing a field must have a reason tied to the contract.

Replay writes in isolated environments or against controlled substitutes. Repeating a production request against two systems can itself send two emails or create two charges. State what the test environment cannot establish about the real dependency.

The runnable API migration example shows why this matters: both implementations return a rejection, while one still changes another tenant’s stored data.

Select cases from the obligations and the risks

Recorded traffic supplies realistic inputs, but it may miss an infrequent failure or unsupported caller. Add boundary values, invalid input, authorization cases and failures suggested by the requirement and hazard analysis. Stateful behavior needs sequences of operations, not only isolated calls.

For every important promise, identify the checks that exercise it and what is still missing. Line or branch coverage can reveal unexercised code. It does not establish that the checks observe the right business outcome.

When old and new disagree, keep a concrete input and the two observations. Decide whether the replacement regressed, the old system has a defect, the change was intended or the intended behavior remains unresolved. Use the difference classification table rather than automatically accepting the old output.

A checklist for one migration boundary

Copy this into the change record. A missing answer stays visible; it does not become a successful check.

  1. Scope: name the component, consumers, revisions and environments being compared.
  2. Agreement: list preserved behavior, intentional changes, supported inputs and the authority for those decisions.
  3. Case selection: map requirements and consequential failure modes to checks, including stateful sequences where needed.
  4. Observations: retain raw outputs, durable state and external effects, with the exact normalization rules.
  5. Results: record what ran, what failed, what remains untested and what must be rerun after a revision changes.
  6. Differences: retain each material discrepancy, its explanation, owner and required follow-up.
  7. Cutover: test supported old/new consumers, define rollout signals and establish a recovery plan for data already changed.
  8. Decision: have the authorized owner assess the current evidence and unresolved risk for this release.

A passing corpus establishes agreement for those observations and cases. It is not a proof of equivalence for every input, execution sequence or production condition.

Keep the checks after the migration

Proof’s requirements and mirror records can organize preserved behavior, implementation links and open gaps. The verification results still need to establish each material claim. Keep the original rationale and representative regression checks so the next agent can understand why the replacement behaves this way.

Moving one boundary at a time can limit the scope of each decision. Martin Fowler’s Strangler Fig account describes incremental replacement; it does not remove the need to verify coexistence and eventual cutover.

Bring one component you’re replacing

Use Proof alongside your migration team to recover the important promises and assess the replacement. If you also need implementation help, scope that work separately through the factory.