DEV Community

codecraft
codecraft

Posted on

Instrument the legacy path before you rewrite it

Every plan to retire a legacy system carries an unstated assumption, which is that once the new implementation exists, somebody will be able to tell whether it is right. That assumption is where most migrations quietly go wrong, because the new service usually ships on time and then sits behind a flag for months while nobody can produce evidence strong enough to justify flipping it. The architecture was never the hard part. Proving equivalence was.

Why do migrations stall after the replacement is written?

Because equivalence is a claim about behaviour under real inputs, and the only place those inputs exist in useful quantity is production. Unit tests encode what the team believes the system does, which is exactly the belief the migration is meant to test, so a green suite tells you very little about a random eight-year-old branch that fires for one customer segment on the last business day of the quarter. Teams that skip the comparison step end up negotiating the cutover in a meeting rather than settling it with data, and meetings tend to resolve in favour of whatever is already running.

The cheapest fix is to make the legacy path emit its inputs and outputs before you write a line of the replacement, then run the candidate against a sample of that traffic and compare the two results. Two constraints keep this from becoming a liability: the candidate must execute off the request path, so a slow or hanging implementation cannot add latency to anything a customer is waiting on, and its exceptions must be caught and recorded rather than propagated, because a comparison harness that degrades production will be switched off within a week and never switched back on.

Where should incremental legacy modernization actually begin?

It begins with the seam and the sampling, rather than the target architecture, because you cannot safely move what you cannot yet observe in both places at once. Pick the narrowest call boundary that fully contains the behaviour you intend to replace, wrap it, and start recording before anything is rewritten, since the recorded traffic then doubles as your specification, your test corpus, and your cutover evidence. Most of the practical difficulty in a legacy system modernization turns out to be plumbing of this kind, and teams that treat it as the first deliverable rather than an afterthought tend to reach a decision far sooner.

Nondeterminism will generate most of your early mismatches

Timestamps, generated identifiers, map iteration order, float accumulation, and anything reading a clock or a random source will all differ loudly while telling you nothing, so inject those dependencies and normalise them before comparison rather than estimating the noise. Precision deserves particular attention, because a legacy engine doing binary floating-point arithmetic and a replacement using decimals will disagree in the last place on a meaningful fraction of records, and you have to decide up front whether that is a bug in the old system, a bug in the new one, or a tolerance you are prepared to write down.

Mismatches need a taxonomy and an owner

Group differences by their shape rather than counting them one by one, since a hundred thousand mismatches usually collapse into six distinct causes once grouped, and a count per cause with a named owner is something a team can actually burn down. Give each class a disposition of new-system bug, legacy bug, or accepted difference, and require that the accepted ones carry a written rationale, because those are the entries someone will read during an incident review years from now.

Do this, and the cutover stops being a decision about confidence and becomes a threshold you agreed on in advance, which is the whole point of incremental legacy modernization in the first place.

Top comments (0)