Your Refactor Is a Guess Until You Diff the Public Surface
A refactor can pass every unit test and still break behavior. The build stays green. The diff looks identical. Then an order sorts differently in production.
That is the quiet failure pattern. Nobody reviewed the public surface. They only reviewed the code.
The Quiet Failure Pattern
Here is a synthetic case based on a common legacy change.
// Before
function orderTotal(list) {
return list.sort(); // lexicographic
}
// After: "cleaner" and still wrong
function orderTotal(list) {
return list.sort((a, b) => a - b); // numeric
}
Both lines look reasonable. For [10, 9, 1], the first returns [1, 10, 9]. The second returns [1, 9, 10].
A client that depends on the old order breaks silently. Unit tests rarely catch this because the old order was never written down.
The lesson is not "sorting is dangerous". The lesson is that intent is not a contract. Behavior is.
Diff the Public Surface, Not the Internals
Internal functions change names and move around. That is fine. The public surface should not change without discussion.
The public surface includes:
- HTTP status codes and response headers
- JSON shape and field order
- Sorting and pagination order
- Precision and rounding of numbers
- Number of database rows written
A messy repo has no tests for these. A golden-master script can still capture them in ten minutes.
The Artifact: A Public-Surface Snapshot
This script probes a few routes and stores normalized responses. It runs against a local staging copy of the app.
#!/usr/bin/env bash
# surface-snapshot.sh — capture observable HTTP behavior
set -euo pipefail
BASE="${BASE:-http://localhost:3000}"
OUT="${OUT:-./snapshots}"
mkdir -p "$OUT"
probe() {
local name="$1"
local path="$2"
curl -sS -o "$OUT/$name.body" -w "%{http_code}" "$BASE$path" > "$OUT/$name.status"
}
probe cart 'cart/totals?ids=10,9,1'
probe user 'users/42'
probe order 'orders/77'
node - "$OUT" <<'NODE'
const fs = require('fs');
const dir = process.argv[2];
const report = {};
for (const file of fs.readdirSync(dir).sort()) {
let text = fs.readFileSync(`${dir}/${file}`, 'utf8');
text = text.replace(/"date":"[^"]+"/g, '"date":"<VALUE>"');
text = text.replace(/"id":"[^"]+"/g, '"id":"<VALUE>"');
report[file] = text;
}
fs.writeFileSync('surface.json', JSON.stringify(report, null, 2));
NODE
Run it once before the refactor:
BASE=http://localhost:3000 ./surface-snapshot.sh
mv surface.json baseline.json
Apply the refactor in a separate worktree. Restart the app. Run the script again:
BASE=http://localhost:3000 ./surface-snapshot.sh
diff baseline.json surface.json
Every line in that diff is a behavior decision. You either accept it deliberately, or you do not merge.
The Workflow That Makes It Useful
Follow these steps in order.
- Start a clean staging copy. The database seed must match before and after.
- Run the snapshot on
main. Save it asbaseline.json. - Generate a probe list for the routes you can identify. I used MonkeyCode's free model access to draft that initial list. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The model had not seen runtime traffic, so I reviewed every probe and kept only the routes that existed in the repo.
- Apply the AI-suggested patch in a worktree. Do not touch
mainyet. - Restart the server and run the snapshot again.
- Compare the two JSON files. If only normalized fields change, the refactor is safe.
- If real fields change, decide: update the baseline or reject the patch.
That decision is now explicit. The reviewer stops guessing.
Where the Free Server Option Fits
The baseline needs a stable home. I keep mine on a free server option so the whole team can fetch the same file. The file is static and versioned. Any static host works; the server is not the point.
The value is that the baseline outlives one reviewer. It becomes part of the next refactor discussion.
Limitations
Golden-master snapshots capture today's behavior, not correct behavior. If the legacy output is a bug, this workflow locks the bug in. Fixing it is a separate decision.
The script only sees the routes you probe. It does not see background jobs, cache timing, or race conditions. It does not measure performance.
Volatile fields will create noise. Normalize dates and generated IDs, or exclude the endpoint entirely.
Who Should Not Use This
Teams already rewriting the whole service should skip this. The baseline churns too fast.
Greenfield projects with real unit tests should use those tests first. The snapshot is a bridge, not a replacement.
The Payoff
Refactoring a messy repo is a measurement problem. Measure the public surface before the change. Measure it again after. Then the smallest safe refactor stops being a hopeful guess.
The script takes ten minutes to write. It pays for itself on the first silent behavior change.
If you keep a public-surface checklist for your own messy module, leave it in the comments. The shortest checklists are usually the most honest.
Top comments (1)
Your approach to highlighting the importance of reviewing the public surface during refactors is spot on. It’s a common pitfall that can lead to silent failures, and your use of a snapshot script for validating changes is a practical solution that any team could implement. I particularly appreciate how you've outlined a clear workflow; it could foster better collaboration and reduce chances of regression. If you’re looking for support in further enhancing this process or exploring automation options around it, I’d love to discuss a potential collaboration. What challenges have you faced while implementing this in team settings?