Module 1: Why Not Rewrite

Why the big rewrite fails

Overview

In the previous lesson you saw the first blow: during a big rewrite, the business receives zero value while the team works, and the backlog of requested features piles up unchecked. This lesson opens that failure mode and shows it's even worse than it seemed, because there's a detail the window of silence hides: the goal moves. When you rewrite a live system, your target isn't fixed —"reach the features the legacy has today"— but moving, because the legacy keeps growing while you run toward it. The business doesn't freeze out of courtesy: it keeps asking for changes, keeps patching bugs, keeps adding rules to the old system, and each of those changes pushes the finish line a little further away. Rewriting a live system is running toward a point that recedes each time you take a step.

This is called the feature parity problem: to be able to turn off the legacy, the rewrite has to do everything the legacy does —not 90%, not "the important stuff," but everything, because each missing function is a complaining customer or a broken process—. And "everything the legacy does" isn't a fixed number: it grows each week. This lesson measures it. We're going to model the feature gap between the legacy and the rewrite over time, across several scenarios of business speed, and you'll see that parity —when it arrives— arrives in years, and that in the scenario where the business runs as fast as the rewrite, parity never arrives: the target recedes at exactly the same pace at which you approach it.

Connection with the module. Lesson 1 measured the window of silence (the rewrite delivers 0); this lesson explains why that window doesn't close on the planned date, or doesn't close ever: feature parity is a moving target. It's the first of the four structural failure modes of the big rewrite. Lesson 3 attacks the second (the second-system syndrome, which inflates the scope) and lesson 4 the third (the tacit knowledge that gets lost). Together they build the case for why the rewrite fails; lessons 5 and 6 build the case for the incremental alternative. The decision of whether to migrate or not —with its formal cost— is the decisions guide; here we only demonstrate why the rewrite path is a trap.

An analogy: catching a train that's already moving

You're standing on the platform and the train starts to move. You decide to catch it by running. If the train were stopped, it would be easy: you run, you catch it, you board. But the train isn't stopped —it accelerates while you run—. Now the question isn't "can I run?", but "do I run faster than the train accelerates?". If yes, you'll catch it, though maybe after an exhausting several-minute sprint. If the train accelerates just as fast as you run, you'll always keep the same distance: you never catch it, no matter how hard you run, because every meter you gain the train gains too. And if the train accelerates faster than you, every second you're farther, not closer.

Rewriting a live system is running after that train. The legacy is the moving train: it doesn't wait for you on the platform. Your rewrite speed is your sprint. And the speed at which the business adds features to the legacy is the train's acceleration. The question that decides everything isn't "can the team rewrite?" —of course it can—, but "does it rewrite faster than the business grows the legacy?". When the business is active —like that of any live marketplace, including Mercado—, the answer tends to be uncomfortable. Let's put numbers on it.

Worked example: feature parity as a moving target

We're going to model the legacy and the rewrite as two feature counters. The legacy starts with the ones it already has (Mercado has, say, 120) and adds a few per month (the business doesn't stop). The rewrite starts at 0 and adds features per month at its own pace. The gap is the difference; parity is reached when the gap hits 0 —and it's only reached if the rewrite closes the gap faster than the legacy opens it—:

import math

LEGACY_START = 120   # features Mercado's monolith already has on day 0
HORIZON = 24         # months we're going to observe


def months_to_parity(legacy_vel, rewrite_vel):
    # The rewrite catches the legacy only if it closes the gap faster than
    # the legacy grows. If not, the target recedes forever.
    if rewrite_vel <= legacy_vel:
        return None
    return math.ceil(LEGACY_START / (rewrite_vel - legacy_vel))


scenarios = [
    # (name, legacy features/mo, rewrite features/mo)
    ("frozen legacy (myth)", 0, 8),
    ("slow business",        3, 8),
    ("active business",      6, 8),
    ("competitive pressure", 8, 8),
]

print(f"{'scenario':<26}{'legacy/mo':>11}{'rewrite/mo':>12}{'parity':>12}")
print("-" * 61)
for name, legacy_vel, rewrite_vel in scenarios:
    m = months_to_parity(legacy_vel, rewrite_vel)
    when = f"month {m}" if m is not None else "NEVER"
    print(f"{name:<26}{legacy_vel:>11}{rewrite_vel:>12}{when:>12}")

# The month-by-month gap in the "active business" scenario (the realistic one for Mercado).
print("\nFeature gap (scenario 'active business', legacy 6/mo vs rewrite 8/mo):")
print(f"{'mo':>4}{'legacy':>9}{'rewrite':>9}{'gap':>9}")
print("-" * 31)
legacy = LEGACY_START
rewrite = 0
for month in range(0, HORIZON + 1):
    if month in (0, 6, 12, 18, 24, 60):
        print(f"{month:>4}{legacy:>9}{rewrite:>9}{legacy - rewrite:>9}")
    legacy += 6
    rewrite += 8

parity = months_to_parity(6, 8)
print(f"\n  At this pace parity arrives at month {parity} "
      f"({parity // 12} years) - and only if nobody raises the legacy's speed.")
print("  Meanwhile: two systems to maintain, zero new value delivered.")

What to expect. When you run the file, the output is exactly this:

scenario                    legacy/mo  rewrite/mo      parity
-------------------------------------------------------------
frozen legacy (myth)                 0           8    month 15
slow business                        3           8    month 24
active business                      6           8    month 60
competitive pressure                 8           8       NEVER

Feature gap (scenario 'active business', legacy 6/mo vs rewrite 8/mo):
  mo   legacy  rewrite      gap
-------------------------------
   0      120        0      120
   6      156       48      108
  12      192       96       96
  18      228      144       84
  24      264      192       72

  At this pace parity arrives at month 60 (5 years) - and only if nobody raises the legacy's speed.
  Meanwhile: two systems to maintain, zero new value delivered.

Read the first table top to bottom, because each row is a step toward the bad news.

The first scenario, "frozen legacy," is the fantasy world in which almost everyone plans the rewrite: it's assumed the legacy stays still (0 features/mo) while the rewrite catches it at 8/mo. In that world parity arrives in month 15: a little over a year, hard but finite. The problem is that that world doesn't exist: the legacy never freezes, because freezing it means telling the business "we're not going to fix a single bug or add a single feature for over a year," and no live business accepts that.

Go down a row. "Slow business": the legacy grows just 3 features/mo. Parity slides to month 24 —from 15 to 24, almost double—, just because the target moved a little. Go down another. "Active business," the realistic scenario for Mercado: the legacy grows 6/mo. Parity jumps to month 60 —five years—. Notice the jump: the rewrite's speed didn't change (still 8/mo), but since the legacy now moves at 6, the gap closes at only 2/mo effective, and closing 120 features at 2/mo takes five years. The speed that matters isn't yours: it's the difference between the two.

And the last row is the one to tattoo. "Competitive pressure": the business, pushed by the competition, adds features to the legacy at 8/mo —exactly the same as the rewrite—. Result: NEVER. The gap stays nailed at 120 forever. Look at the second table to watch it happen: in the "active business" scenario, the gap starts at 120 and drops very slowly (108, 96, 84, 72...); in the competitive-pressure one it doesn't even drop, because every feature the rewrite adds, the legacy adds too. You run with all your soul and the train keeps the distance. You never board.

That's the failure mode in its purest form. The big rewrite doesn't fail because the team is slow or lazy —in all these scenarios the rewrite produces 8 features a month, an excellent pace—. It fails because it was chasing a moving target, and the only way to catch it was to ask the business to stop moving, which is precisely what the business can't do.

Deep dive: why parity has to be total (and why that hurts)

You could object: "I don't need total parity; I turn off the legacy when the rewrite has the important stuff." Here's the trap: in a legacy system, you don't know what "the important stuff" is. The legacy's 120 features include a great many that seem trivial or dead, but that some customer uses, some nightly process depends on, or some external integration calls. Lesson 4 measures it in detail —the tacit knowledge—, but the point for this lesson is arithmetic: as long as the rewrite doesn't cover 100%, you can't turn off the legacy, so you have to keep both systems running in parallel. And maintaining two systems costs more than maintaining one: each bug has to be fixed twice, each urgent feature that can't wait for the rewrite has to be put into the legacy (moving the target even further), and each data change has to be reflected in both.

That's why the last line of the output says what it says: "two systems to maintain, zero new value delivered." During the years the chase lasts, the team not only doesn't deliver new value (it's busy rewriting the old): it also pays the overhead of operating two systems at once. It's the worst of both worlds.

Compare this with incremental, which lesson 5 develops. In an incremental migration there's never a gap of 120 features to close, because you don't rewrite the whole system: you rewrite one slice, put it in production, and that slice has already reached parity (it's small, its parity is achievable in weeks), you turn it off in the legacy, and you move to the next. The system's total parity is reached slice by slice, each one a small and still target, instead of one giant and moving target. Incremental doesn't win the race against the train: it boards at the first station, rides a stretch, gets off, and repeats. It never runs after the whole train.

flowchart LR
    subgraph Rewrite["Big rewrite: one giant, moving target"]
      A["rewrite<br/>(0 -> 120+)"] -. chases .-> B["legacy<br/>(120 and growing)"]
    end
    subgraph Incremental["Incremental: small, still targets"]
      S1["slice 1<br/>(parity in weeks)"] --> S2["slice 2"] --> S3["slice 3"] --> S4["..."]
    end

Common mistakes

Planning the rewrite assuming the legacy freezes. What happens: the rewrite plan estimates the parity date counting only the legacy's current features, as if from day 1 nobody touched the old system. Why it happens: it's the only way for the plan to give an acceptable date —if you counted the legacy's growth, the date would go to years—, so the freeze is assumed without saying it. How to spot it: ask "does this plan assume we won't add a single feature or fix a single bug in the old system during the entire rewrite?". If the honest answer is "yes," the plan lives in the "frozen legacy" scenario that the table showed to be unreal. How to fix it: estimate the parity date with the real speed of the business (the "active business" or "competitive pressure" scenarios), not the fantasy one. When you do, the date usually jumps so much that the rewrite stops being defensible on its own —and that jump is exactly the argument for incremental.

Confusing "the team is fast" with "we're going to reach parity." What happens: the team measures its success by its own speed ("we produce 8 features a month, we're doing great!") and concludes that parity is close. Why it happens: your own speed is visible and motivating; the legacy's speed is another team's, or the business's, and isn't counted on the same board. How to spot it: if the rewrite's progress report shows "features built" but not "gap against the legacy," the half that matters is missing. How to fix it: the metric that decides isn't your speed, it's the difference between your speed and the legacy's. A team at 8/mo against a legacy at 6/mo closes at 2/mo effective —very slow—; the same team against a legacy at 8/mo never closes. Measure the gap, not your effort. Lesson 7 revisits this: the rewrite is only safe when the legacy can be frozen (small system, no active business on top), and Mercado isn't that case.

Freezing features "temporarily" to win the race. What happens: seeing that parity isn't reached, the team asks to freeze the legacy —"just a few months, so the rewrite catches up"—. Why it happens: it's the apparent way out of the moving target: if you stop the train, you catch it. How to spot it: the policy of "no more changes to the old system except emergencies" appears. How to fix it: recognize that the freeze is lesson 1's window of silence, under another name —you're asking the business for the two years without delivery that we said it can't give—. And it almost never holds: the "emergencies" that do sneak into the legacy move the target again, and now on top of that you have to rewrite those emergencies too. The freeze doesn't solve the moving-target problem; it disguises it. The real way out isn't to stop the train: it's not to chase it, and to migrate by slices.

Exercises

Exercise 1 — Compute the parity. Using the example's formula (months = ceil(initial_features / (rewrite_speed - legacy_speed))), compute the parity month for a system with 90 initial features, a rewrite that produces 10 features/mo, and a legacy that grows at 7 features/mo. Then say what happens to that same rewrite if the legacy's speed rises to 10/mo, and explain why.

See solution

With legacy at 7/mo: the rewrite closes the gap at 10 - 7 = 3 features/mo effective. Parity = ceil(90 / 3) = 30 months, two and a half years. Already very long for a system that, on top of that, has to be maintained in parallel all that time.

If the legacy rises to 10/mo: the gap closes at 10 - 10 = 0 features/mo. The formula doesn't apply (division by zero), and the result is NEVER: the rewrite and the legacy advance at the same pace, so the initial gap of 90 stays nailed at 90 forever. Why: the speed that closes the gap isn't the rewrite's in the abstract, but its advantage over the legacy. When that advantage is zero, it doesn't matter how fast the team runs —it produces 10 features a month, an excellent pace— because the legacy produces another 10 at the same time. It's the "competitive pressure" scenario of the table: running full out and not getting a meter closer.

Exercise 2 — Why parity has to be total. A manager proposes: "let's not wait for 100% parity; let's turn off the legacy when the rewrite has 95% of the features, and we'll add the missing ones later." Give two reasons, using this lesson's and lesson 1's ideas, why that missing 5% can cost much more than its size suggests.

See solution
  1. You don't know which features are that 5%. In a legacy system without documentation, you can't consciously choose which are the 6 "sacrificeable" features out of 120. That 5% may include the billing rule a customer who invoices millions depends on, a nightly process nobody remembers until it doesn't run, or an integration with an external partner. Turning off the legacy with that 5% uncovered means betting that none of those six things mattered —and lesson 4 shows that the "weird" parts of the legacy tend to be exactly the ones that matter most—.
  2. Each missing feature breaks the promise of turning off the legacy. If, on turning off the legacy, you discover that functions that were in fact used were missing, you have to turn it back on (or run both systems again), and you're back to the deep dive's worst of worlds: two systems in parallel, double maintenance. The 100% parity isn't perfectionism: it's the literal requirement to be able to turn off the old system, which is the only point at which the rewrite starts to pay. A 95% that forces you to maintain both systems is, in cost terms, almost like a 0%.

Note the contrast with incremental (lesson 5): there you can turn off in parts, because each slice reaches its own 100% (small and verifiable) before turning off, instead of betting the whole system's 100% all at once.

Exercise 3 — The moving target in your words. Explain, without using code, why the assertion "if we give it enough time, the rewrite will eventually catch the legacy" is false in general. Under what exact condition does it become true?

See solution

The assertion is false because it assumes the gap closes over time by the mere fact of waiting, and that only happens if the rewrite advances faster than the legacy. If the legacy grows at the same pace as the rewrite, waiting closes nothing: the gap is constant, and "enough time" is infinite. If the legacy grows faster than the rewrite, waiting widens it: each month that passes you're farther, not closer. Time isn't an automatic ally; it only helps if you're already winning the speed race.

The exact condition under which it does become true is: the rewrite's speed must be strictly greater than the speed at which the legacy grows (rewrite_vel > legacy_vel). Only then does the gap shrink each month and parity is reached in a finite time (initial_features / (rewrite_vel - legacy_vel)). In practice, that condition holds when the legacy can be frozen —small system, no active business on top, little feature pressure—, which is exactly the narrow region where a rewrite is defensible (lesson 7). Mercado, with an active business that doesn't stop, isn't in that region.

Summary and next step

In this lesson you opened the big rewrite's first structural failure mode: feature parity is a moving target. You saw, with the train that accelerates while you run, that rewriting a live system isn't reaching a fixed goal but chasing one that recedes, because the legacy keeps growing. And you measured it: in the realistic scenario of an active business parity arrives at month 60 —five years—, and in the competitive-pressure one it never arrives, because the gap closes at the speed of your advantage over the legacy, not your absolute speed. Besides, until you reach 100%, you have to maintain both systems in parallel: the worst of both worlds.

Before moving on you should be able to: explain why the speed that matters is the difference, not your own; compute the parity month with the gap formula; argue why parity has to be total to be able to turn off the legacy; and detect a rewrite plan that assumes, without saying it, that the legacy freezes.

Lesson 3 attacks the second failure mode, and it's one that doesn't even need the business to move: the second-system syndrome. Even with the legacy frozen and the target still, the rewrite tends to fail for a reason internal to the team itself —the temptation to use the "fresh start" to cram in everything ever wanted—, and you'll measure how that scope inflation slides the closing date from month 20 to 50, or to infinity.

Resources

  • Joel Spolsky, "Things You Should Never Do, Part I" (2000) — joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i. The Netscape case: while they rewrote from scratch, the competition (Internet Explorer) kept advancing and ate their market. This lesson's moving target, told with a real company that disappeared because of it. In English.
  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), ch. 1, "Just Enough Microservices" — why incremental migration avoids the parity race: each slice reaches its own small goal instead of chasing the whole system. In English.
  • Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. The counterpoint: replace in parts instead of running after the whole system. The metaphor that lesson 6 develops. In English.
  • Chad Fowler, "Legacy Code" and the talk "Software as a Reflection of Values" — on why live systems never stop changing and what that implies for modernizing them. A good conceptual complement. In English.