DEV Community

137Foundry
137Foundry

Posted on

How to Run Old and New Systems in Parallel Without Doubling Your Workload

Running a legacy system and its replacement side by side, comparing outputs, is the single best way to catch gaps a migration audit missed. It's also expensive if done naively, because someone has to actually watch both systems and reconcile differences. Here's how to structure a parallel run that catches real problems without burning out whoever's assigned to monitor it.

Step 1: Feed both systems the same inputs automatically

Manually re-entering the same data into two systems is slow and introduces its own errors. Set up a fork at the input layer, whether that's a message queue consumer, an API gateway, or a batch job, that sends every real input to both the legacy system and the replacement simultaneously. The legacy system's output remains authoritative during this phase; the replacement's output is being validated, not yet trusted.

def process_transaction(data):
    legacy_result = legacy_system.process(data)
    new_result = new_system.process(data)
    if legacy_result != new_result:
        log_discrepancy(data, legacy_result, new_result)
    return legacy_result  # legacy stays authoritative during validation
Enter fullscreen mode Exit fullscreen mode

a network operations center with rows of monitors
Photo by ThisIsEngineering on Pexels

Step 2: Automate the comparison, don't do it by eye

Diffing two outputs manually works for the first ten transactions and becomes untenable at any real volume. Write an automated comparison that flags discrepancies and logs them with enough context (the input, both outputs, a timestamp) to investigate later without needing to reproduce the exact conditions from scratch. This is the step that turns a parallel run from "hopefully someone notices something's wrong" into an actual verification process.

Step 3: Triage discrepancies by pattern, not by individual occurrence

Once discrepancies start accumulating, group them by root cause rather than investigating each one individually. A single missed edge case (a specific date range, a specific customer type) might produce hundreds of individual discrepancy log entries that are all actually the same underlying bug. Fixing the pattern once, rather than triaging each occurrence separately, is how a parallel run scales past the first few days without becoming a full-time job for whoever's assigned to it.

Step 4: Set an explicit exit criteria before you start

Decide in advance what "the parallel run is done" looks like: a specific number of consecutive days with zero unexplained discrepancies, or a specific percentage of transaction volume validated without issue. Without an explicit target, parallel runs tend to continue indefinitely out of caution, which delays the actual benefit of retiring the legacy system and keeps two systems in production maintenance simultaneously longer than necessary.

Step 5: Cover a full business cycle if the system has one

If the legacy system handles anything with a monthly, quarterly, or annual pattern (billing cycles, tax calculations, seasonal logic), make sure the parallel run's duration actually covers that cycle at least once. A parallel run that only spans two weeks will miss a bug that only manifests during month-end processing, and that gap won't surface until the legacy system is already gone and there's nothing left to compare against.

Handling discrepancies that turn out to be the legacy system's bug

Not every discrepancy means the new system is wrong. Occasionally the parallel run surfaces a case where the legacy system was actually producing an incorrect result all along, one nobody had noticed because nothing downstream ever checked it carefully. This is a genuinely useful finding, but it needs a deliberate decision, not a default assumption either way. Confirm which system is actually correct against the real business rule (not just which one is older) before deciding whether the "discrepancy" is really the new system introducing a regression or the audit accidentally surfacing a bug in a system everyone had assumed was reliable.

What this catches that a code review can't

The value of an actual parallel run, as opposed to a thorough code review of the replacement, is that it exercises real production data with all its genuine messiness: malformed inputs nobody anticipated, edge cases that occur rarely enough that no test suite happened to cover them, timing-dependent behavior that only shows up under real load. Code review catches what reviewers think to look for. A parallel run catches what actually happens.

137Foundry's engineering team builds this kind of parallel-run infrastructure as a standard phase of legacy migration work, treating it as a verification gate rather than an optional nice-to-have. The complete legacy retirement framework covers how this fits alongside the audit and knowledge-recovery work that should happen before the parallel run even starts.

References

  • Wikipedia's entry on legacy systems for background on why these systems accumulate the kind of edge-case behavior that a parallel run is designed to catch.
  • Martin Fowler's website covers software architecture patterns including strangler fig migrations, a closely related approach to gradually replacing a legacy system rather than a single cutover.
  • MDN's web docs is a useful general reference if the comparison layer you're building involves web APIs or HTTP-level request forking.

A parallel run is real infrastructure work, not a formality. Budgeted properly, it's also the cheapest insurance available against shipping a replacement system that's subtly, silently wrong in ways nobody catches until months later.

Deciding when a parallel run isn't worth building

Not every migration justifies this level of infrastructure. For a low-volume, low-stakes internal system with a thorough audit behind it, the engineering cost of building input forking and automated comparison can exceed the actual risk being mitigated. Reserve the full parallel-run treatment for systems where a wrong answer has real business consequences, financial, legal, or customer-facing, and lean on a simpler validation approach, like spot-checking a sample of transactions manually, for lower-stakes systems where the infrastructure investment wouldn't pay for itself.

A note on team morale during the validation period

Running two systems in parallel for weeks or months is genuinely tedious for whoever's assigned to monitor it, especially once the interesting discrepancies have mostly been found and fixed and what remains is mostly confirming that things continue to work correctly. Rotating this responsibility across the team, rather than assigning it permanently to one person, keeps the monitoring rigorous instead of becoming an afterthought that one burned-out engineer checks less and less carefully as the weeks go on. It's a small operational detail, but it's one of the more common reasons parallel runs quietly stop catching real problems well before their planned end date.

Pair the rotation with a lightweight weekly summary, even a few sentences, of what the past week's discrepancies looked like and whether the rate is trending down. This keeps the whole team oriented on whether the validation period is actually converging toward "safe to cut over" or stalling on a recurring issue that needs deeper investigation, rather than everyone individually assuming someone else is tracking the trend.

A short summary is enough. The point isn't a formal report, it's making sure the trend is visible to the whole team rather than living only in whoever happens to be on monitoring duty that particular week, which is what actually lets the team collectively decide when the exit criteria has genuinely been met, rather than one person's gut feeling carrying that decision alone.

Top comments (0)