Module 8: Project — Modernizing a Slice of Mercado

Declare done and retire the legacy

Overview

The burn-down reached zero (L6) and the fitness function is green. It seems you finished. But "seems" is exactly the word that the method's sixth step —module 7 in its final stretch— exists to eliminate. This step does two things: it declares done with a hard and verifiable criterion, and it retires the legacy for real. And it closes with the underlying question of the whole guide: why did modernizing by slices beat the rewrite? —answered not with opinions, but with what the lessons measured—.

A migration's done isn't a single metric; it's a list of conditions that all must be met. Because the migration cuts the legacy on several fronts that finish at different moments, and the most visible one (the traffic) reaches zero first while others —less visible— lag behind. For the catalog, done is five conditions, one for each step of the method: legacy_calls == 0 (the burn-down, L6), fallback == 0 (the strangler, L3), discrepancies == 0 (the data, L5), legacy_refs == 0 (the fitness, L6), and the characterization green (the golden master, L2). Only when all five are checked, done = True, and the action it triggers isn't "turn off" but delete the legacy: the code, the deploy, and the tables. Done means the slice was modernized end to end and the legacy can be removed without breaking anything, not that it stopped receiving traffic.

And since it's the method's last step, this step also gives an account. The justification of why the incremental path beat the big-bang isn't a speech: it's a number. Throughout the capstone, the incremental method caught problems before production that the big-bang would have sent without verification —the price regressions the golden master detected (L2), the volume purchases the fallback revealed without exposing them (L3), the data discrepancies the parallel-run found before the switch (L5)—. You're going to count those problems and see, in a figure, the difference between modernizing while measuring each step and betting everything on an unverified copy.

Connection with the module. This is the method's sixth and last step (M7), and it consumes the outputs of all the earlier steps: each done condition is the zero a step produced (L6's burn-down, L3's fallback, L5's discrepancies, L6's references, L2's characterization). It's the step that declares the slice closed and collects the migration's reward. Notice the boundary: here we define the criterion of completion and retire the slice. The decision to modernize —the ADR, reversibility as a design property, the record of the why— belongs to architecture-decisions-and-tradeoffs; here we execute the technical close, not the record of the decision. Lesson 8 closes the guide and tells you where to go next.

An analogy: the work handover with a signed certificate and no scaffolding

When you hire a remodel, there's a formal moment called the work handover: the day you declare the work finished, sign a certificate, and make the final payment. That moment doesn't depend on it "looking finished" or on the foreman saying "it's done". It depends on a checklist: does the electricity work? are there no leaks? are the finishes complete? did they remove the scaffolding, the debris, and the tools? did they hand you the keys? Only when all the boxes are checked do you sign the certificate. If one is missing —an unpainted room, a leak, the scaffolding "in case they come back"— the work is not accepted, no matter how impeccable 95% looks.

Notice two details of the work handover that are the point of this lesson. First: it's a list, not a single thing. You don't sign because "the kitchen came out nice"; you sign because everything on the list is done. A work can have a perfect kitchen and a leak in the bathroom, and it isn't finished. Second, and more important: the handover requires that the scaffolding be taken away. A work where everything functions but they left the scaffolding "in case they have to come back" isn't finished —it's half-done, with the house occupied by structures that no longer serve, getting in the way and costing the scaffolding rental—. The work is finished when you can live in the house and the scaffolding is gone.

The migration's done is that work handover. The list is the five conditions: all checked, or it isn't finished. And taking away the scaffolding is deleting the legacy —the code, the deploy, the tables—. A migration where the traffic is at zero but the legacy is still deployed "just in case" is the work with the scaffolding still up: it looks finished, but it isn't, and the scaffolding costs. Done is signing the certificate and watching the scaffolding truck drive away.

Worked example: the done in green and the measured justification

We're going to execute the step in two parts. Part 1 runs the done criterion as a checklist of five conditions over the catalog, in two states: state A, "almost there" —the visible conditions green, but one reference to the legacy left in the code— and state B, truly finished —the five green—. Only the second authorizes deleting and retiring the legacy. Part 2 produces the measured justification: it counts the problems the incremental method caught before production, that the big-bang would have sent without verification.

# Step 6 of the method: declare done and turn off the legacy. Done is NOT a single
# condition but SEVERAL (all verifiable): calls to the legacy at 0, fallback at
# 0, discrepancies at 0, references to the legacy at 0, characterization green.
# Only with all green is the legacy DELETED. And the measured justification: why
# incremental beat the rewrite, over what the lessons actually measured.

# --- PART 1: the done criterion, with its 5 conditions. ---
def done_report(state):
    checks = {
        "legacy_calls == 0":      state["legacy_calls"] == 0,        # burn-down (L6)
        "fallback == 0":          state["fallback"] == 0,            # strangler (L3)
        "discrepancies == 0":     state["discrepancies"] == 0,       # data (L5)
        "legacy_refs == 0":       state["legacy_refs"] == 0,         # fitness (L6)
        "characterization green": state["characterization_green"],   # golden master (L2)
    }
    return checks, all(checks.values())


def show(title, state):
    checks, is_done = done_report(state)
    print(title)
    for name, ok in checks.items():
        print(f"    [{'x' if ok else ' '}] {name}")
    if is_done:
        print("    => done = True  ->  DELETE the legacy (code + deploy + tables)\n")
    else:
        pend = [n for n, ok in checks.items() if not ok]
        print(f"    => done = False ->  missing: {', '.join(pend)}\n")
    return is_done


print("The slice's done: ALL the conditions, not just one\n")

# State A: "almost there". The visible conditions are green, but 1 reference to
# the legacy is left in the code. It looks finished; it is NOT.
show("State A - 'almost there' (traffic and data at 0, but 1 reference left):", {
    "legacy_calls": 0, "fallback": 0, "discrepancies": 0,
    "legacy_refs": 1, "characterization_green": True,
})

# State B: truly finished. The 5 conditions green.
done_B = show("State B - truly finished:", {
    "legacy_calls": 0, "fallback": 0, "discrepancies": 0,
    "legacy_refs": 0, "characterization_green": True,
})

legacy_retired = done_B
print(f"  catalog_legacy_deleted = {legacy_retired}")
print("  Done is not 'the traffic is on the new': it's the 5 conditions green and")
print("  the legacy DELETED -code, deploy, and tables-, not 'off just in case'.\n")


# --- PART 2: the measured justification. What would have reached production with the
#     big-bang, and what the incremental method caught BEFORE production. ---
caught = [
    ("L2 characterize", "golden master caught price regressions",       3),
    ("L3 facade",       "fallback detected uncovered purchases (bulk)", 100),
    ("L5 migrate data", "parallel_run caught data discrepancies",       2),
]

print("Measured justification: incremental vs big-bang (what each lesson measured)")
print(f"  {'step':<18}{'what it caught BEFORE production':<45}{'#':>4}")
print("  " + "-" * 67)
total = 0
for step, what, n in caught:
    total += n
    print(f"  {step:<18}{what:<45}{n:>4}")
print("  " + "-" * 67)
print(f"  {'TOTAL problems caught before production':<63}{total:>4}")
print("\n  The big-bang (turn off Mercado and copy everything) would have sent the 105 to")
print("  production without verification, with the business off and no way back.")
print("  The incremental caught them slice by slice, with Mercado alive, each step")
print("  reversible and measured. That's why incremental beat the rewrite: not on faith,")
print("  but on 105 problems caught before they touched a customer.")

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

The slice's done: ALL the conditions, not just one

State A - 'almost there' (traffic and data at 0, but 1 reference left):
    [x] legacy_calls == 0
    [x] fallback == 0
    [x] discrepancies == 0
    [ ] legacy_refs == 0
    [x] characterization green
    => done = False ->  missing: legacy_refs == 0

State B - truly finished:
    [x] legacy_calls == 0
    [x] fallback == 0
    [x] discrepancies == 0
    [x] legacy_refs == 0
    [x] characterization green
    => done = True  ->  DELETE the legacy (code + deploy + tables)

  catalog_legacy_deleted = True
  Done is not 'the traffic is on the new': it's the 5 conditions green and
  the legacy DELETED -code, deploy, and tables-, not 'off just in case'.

Measured justification: incremental vs big-bang (what each lesson measured)
  step              what it caught BEFORE production                #
  -------------------------------------------------------------------
  L2 characterize   golden master caught price regressions          3
  L3 facade         fallback detected uncovered purchases (bulk)  100
  L5 migrate data   parallel_run caught data discrepancies          2
  -------------------------------------------------------------------
  TOTAL problems caught before production                         105

  The big-bang (turn off Mercado and copy everything) would have sent the 105 to
  production without verification, with the business off and no way back.
  The incremental caught them slice by slice, with Mercado alive, each step
  reversible and measured. That's why incremental beat the rewrite: not on faith,
  but on 105 problems caught before they touched a customer.

Read the two parts, because together they close the method: the first declares the slice finished, the second says why the path we took was the right one.

State A: "almost there", the most dangerous. Look at its boxes: legacy_calls == 0 checked, fallback == 0 checked, discrepancies == 0 checked, characterization green checked. Four of five, and the most visible ones —the traffic is on the new, no purchases falling to the legacy, the data matches, the characterization passes—. A team that only looked at those four would say "we're done". But one box is still unchecked: legacy_refs == 0 (1 reference is left in the new code that still calls the legacy). The verdict: done = False, legacy_refs == 0 missing. The migration seems finished wherever you look first, but it isn't: if you deleted the legacy now, you'd break the modern, which still depends on that reference. It's the work with the perfect kitchen and a leak in the bathroom.

State B: the complete work handover. The five boxes checked. done = True. And notice the action it triggers: not "turn off the legacy", but DELETE the legacy (code + deploy + tables). Now the entire legacy catalog module can be removed —the code in the repository, the deployed service, the tables nobody uses anymore— without breaking anything, because nothing depends on it anymore. The catalog_legacy_deleted = True seals it: the slice was modernized end to end and the scaffolding is gone. Done isn't "the traffic is on the new"; it's the five conditions green and the legacy deleted —not "off just in case"—.

The measured justification. Part 2 answers the guide's underlying question with a figure. Throughout the capstone, the incremental method caught 105 problems before they touched a customer: the 3 price regressions the golden master detected when a "clean" reimplementation changed the numbers (L2), the 100 volume purchases the fallback revealed the modern didn't cover —without exposing a single user to a price error— (L3), and the 2 data discrepancies the parallel-run found before the read-switch (L5). The big-bang —turn off Mercado over a weekend and copy everything— would have sent the 105 to production without verification, with the business off during the window and no way back if something went wrong. The incremental caught them slice by slice, with Mercado alive, each step reversible and measured. That's why incremental beat the rewrite: not on faith or preference, but on 105 problems caught before they reached a customer.

Deep dive: why done is a list and why done is deleting

Two ideas of this step deserve development, because they're the ones most resisted in practice.

Why a list, not a metric. The migration cuts the legacy on several fronts at once, and those fronts don't finish at the same time. The traffic is diverted (the strangler's front, L3); the fallback closes when the modern covers all the cases (L6); the data discrepancies are reconciled (the data's front, L5); the references in the code are removed (the fitness's front, L6); the characterization stays green (the golden master's front, L2). Each front reaches zero at a different moment, and it's common for the traffic to arrive first (it's the most visible and what the business pushes) while a code reference or the fallback lag behind. If you define done by the most visible front, you declare finished with open fronts —and those open fronts are what prevents deleting the legacy—. The list forces you to verify all the fronts. Done is the conjunction (all(...)), not the flashiest box.

   Front                 Condition                From       Reaches 0...
   ───────────────────   ──────────────────────   ────────   ─────────────────────
   traffic               legacy_calls == 0        L6         first (visible)
   modern completeness   fallback == 0            L3/L6      with the bulk (stubborn stretch)
   data                  discrepancies == 0       L5         after reconciling
   code                  legacy_refs == 0         L6         at the end (last dependency)
   behavior              characterization green   L2         holds the whole time
                                               ┌──────────────────────────────┐
   done = AND of the five  ───────────────────>┤ only then: DELETE            │
                                               └──────────────────────────────┘

Why deleting and not turning off. "Turning off just in case" sounds prudent and is, in reality, the gateway to the eternal migration. Leaving the legacy deployed but disconnected has three real costs that don't disappear by being "off": the operating cost (it keeps consuming servers, licenses, maintenance —you pay for a system that serves no one—), the cognitive cost (every new developer finds it in the code, doesn't know it's dead, and loses time understanding it or modifies it believing it's alive), and the risk cost (dead code without maintenance rots —dependencies unupdated, vulnerabilities unpatched— and it's a trap: someone could reactivate it by mistake and reintroduce the bugs the migration left behind). Deleting eliminates all three; turning off eliminates none. And the fear that motivates the "just in case" —"what if we need it?"— has a technical answer: the code isn't lost when you delete it, git's history preserves it. Deleting the legacy from the live system isn't throwing it in the trash; it's taking it out of where it gets in the way, with a copy kept in case the improbable case happens. Done is deleting because only deleting collects the migration's reward —shrinking the system, no longer maintaining two—.

One last precision: done is defined at the start, not at the end. The checklist of the five conditions is written on day one of the migration, in the plan, so everyone knows which finish line they're running toward. Defining done at the end —when you're already "almost"— is too late: by then everyone has their own idea of "finished", and the temptation to declare victory on the most visible front is at its maximum. Done written in advance is a contract with the future: "we won't say we're finished until these five boxes are checked and the legacy deleted".

Common mistakes

Declaring "migrated" with the old route still on. What happens: the traffic reaches zero, the team declares the migration finished, and the legacy stays deployed "just in case". Why it happens: legacy_calls == 0 is the most visible box and the one the business pushes, and deleting the legacy is extra work with no immediate visible reward. How to spot it: the migration was declared "done" but the legacy is still in production, shows up in the dashboards, and consumes resources months later. How to fix it: done isn't "traffic at zero", it's the five conditions met and the legacy deleted. Traffic at zero is necessary but not sufficient —the fallback, the discrepancies, the references remain—. The migration finishes when the legacy is deleted, not when it stopped receiving requests. Putting "deleted" in the definition of done from day one avoids this premature declaration.

Defining done by the most visible metric instead of by the complete list. What happens: the team looks only at the traffic burn-down and, seeing it at zero, considers the migration finished, without verifying the fallback, the discrepancies, or the references. Why it happens: the traffic is the most visible and easy to measure; the other fronts are less visible. How to spot it: the migration is declared done with open fronts —the fallback still greater than zero, a reference to the legacy that was left—; when you try to delete the legacy, something breaks. How to fix it: done is the conjunction of the five conditions (all(...)), not the flashiest box. Each front of the migration has its own condition and reaches zero at a different moment; the list forces you to verify all of them. If when trying to delete the legacy something breaks, it's the proof that done was badly defined —a box was missing—.

Confusing "the techniques worked" with "the slice is modernized". What happens: the team executed the six steps, each one "went well", and declares the modernization finished —but the legacy is still on, or the fallback is still greater than zero, or a reference was left—. Why it happens: six executed techniques feels like six achievements, and "finished" sounds like "I did everything on the list". How to spot it: ask "was the catalog's legacy deleted?". If the answer is "no, it's still there just in case" or "the traffic is on the new but the old is still there", the slice isn't modernized. How to fix it: the capstone ends in a number, not in a list of activities: done = True with the legacy deleted. Executing the techniques is the means; the end is the slice closed and the legacy retired. This lesson's done criterion is what distinguishes one thing from the other.

Exercises

Exercise 1 — Done or not? For each state of the catalog, say whether it's done and what's missing: (a) the five conditions green; (b) legacy_calls=0, fallback=0, discrepancies=0, legacy_refs=0, characterization red; (c) legacy_calls=0, fallback=5, discrepancies=0, legacy_refs=0, characterization green; (d) legacy_calls=0, fallback=0, discrepancies=2, legacy_refs=1, characterization green.

See solution
  • (a) DONE. The five conditions green: the legacy can be deleted. It's the example's state B.
  • (b) NOT done. characterization green is missing: a characterization in red means the catalog's behavior changed relative to the golden master —the modern doesn't reproduce some case—. Even though the traffic, the fallback, the discrepancies, and the references are at zero, deleting the legacy would leave the modern serving a behavior different from the one it promised to preserve.
  • (c) NOT done. fallback == 0 is missing: 5 requests still fall to the legacy through fallback —the modern has a gap (a case it doesn't cover)—. Turning off the legacy would leave those 5 requests unserved. The traffic_percent can be at 100, but the fallback greater than zero says the modern isn't complete.
  • (d) NOT done. Two are missing: discrepancies == 0 (2 data rows are left that don't match between old and new) and legacy_refs == 0 (1 reference is left in the code). Only the traffic and the fallback are at zero —the deceptive state, advanced on the visible boxes but with two open fronts—.

The rule is always all(...): done only if the five are checked. A single unchecked one means something still depends on the legacy or the behavior wasn't preserved, and while that's the case, it can't be deleted.

Exercise 2 — The measured justification. The incremental method caught 105 problems before production. (a) Distribute the 105 among the steps that caught them. (b) What would have happened with each group if a big-bang had been done? (c) Why is this justification stronger than saying "incremental is better because it's safer"?

See solution

(a) The 105 are distributed like this: 3 price regressions caught by the golden master when comparing a "clean" reimplementation against the frozen behavior (L2); 100 volume purchases the fallback revealed the modern didn't cover, without exposing a user (L3); and 2 data discrepancies (a missing row, a mistranslated field) the parallel-run found before the read-switch (L5). 3 + 100 + 2 = 105.

(b) With a big-bang, each group would have reached production without verification: the 3 price regressions would have been served to customers (one giving away products for free, others breaking a partner's reconciliation); the 100 volume purchases would have been miscalculated or failed (the modern didn't cover the bulk); and the 2 data discrepancies would have left one product disappeared and another at a tenth of its price. All discovered through complaints and losses, not through comparisons —and with the business off during the copy window, no way back—.

(c) Because "it's safer" is an opinion that can be argued; 105 problems caught before production is a measured fact that comes out of running each lesson's code. The justification doesn't appeal to an aesthetic preference for the incremental; it shows, with a concrete figure, exactly what would have failed with the big-bang and what the incremental caught. A measured argument resists the "but the big-bang is faster" in a way a principled argument doesn't: it doesn't say "trust me", it says "here are the 105 problems the big-bang would have sent you to production".

Exercise 3 — Write the done for another slice. You're going to start the migration of Mercado's orders and they ask you to define its done on day one. (a) Write the checklist of conditions. (b) Why is it better to define it now than when you're "almost"? (c) What concrete action does checking all the boxes trigger?

See solution

(a) The done checklist for orders, one per front: (1) legacy_calls == 0 —no orders request touches the legacy monolith—; (2) fallback == 0 —the modern covers all the cases, no request falls to the legacy through fallback—; (3) discrepancies == 0 —the orders data migrated and verified row by row—; (4) legacy_refs == 0 —the new orders code has no reference to the monolith—; (5) characterization green —the behavior of orders (including its quirks) was preserved relative to the golden master—. (An orders-specific condition could be added, like "all the nightly batch processes repointed to the modern", because orders has more batch flows than the catalog.) Done only if all are met.

(b) Because defining done at the start is a contract with the future: it sets the finish line before the pressure and diverging opinions appear. When you're "almost", everyone will have their idea of "finished", and the temptation to declare victory on the most visible front (the traffic) will be at its maximum —the weakest moment to define the criterion—. A done written on day one resists that pressure: "we agreed that we're not finished until these five boxes are checked and the legacy deleted".

(c) Checking all the boxes triggers deleting the entire orders legacy: the module's code in the repository, the service/deploy if it had its own, and the legacy tables nobody uses anymore —with a possible short grace period and an expiry date before the definitive deletion—. The action isn't "declaring in a meeting that we're finished" or "turning off just in case"; it's the concrete deletion that collects the reward: orders stops being part of the legacy monolith, and the monolith is one slice smaller.

Summary and next step

In this lesson you took the method's sixth and last step: declaring done and retiring the legacy (module 7). You executed the done criterion as a list of five conditions that all must be metlegacy_calls == 0 (L6), fallback == 0 (L3), discrepancies == 0 (L5), legacy_refs == 0 (L6), characterization green (L2)—, and you saw, with the work handover that's only signed with the whole list checked and the scaffolding removed, that done is a checklist (not the most visible box) and that done is deleting (not turning off just in case). State A "almost there" gave done = False (a reference was missing); state B, with the five green, gave done = True → DELETE, and the catalog's legacy was retired for real. And you closed with the measured justification: the incremental method caught 105 problems before production (3 price regressions, 100 uncovered purchases, 2 data discrepancies) that the big-bang would have sent without verification, with the business off and no way back. Incremental beat the rewrite not on faith, but on a figure.

Before moving on you should be able to: write the done checklist of a migration; evaluate a state and say whether it's done and what's missing; explain why done is a list and why done is deleting; and defend, with numbers, why incremental beat the big-bang.

Lesson 8 is the capstone deliverable and the close of the whole guide. You're going to integrate the six steps you executed separately —characterize, facade, extract, migrate data, measure, done— into a single program that runs the catalog's complete modernization end to end, with its literal output phase by phase, ending in done = True and the legacy deleted. And you're going to close the guide: the complete method in your hands, and the map of where to go next —architecture-decisions-and-tradeoffs, event-driven-architecture, architectural-styles-and-boundaries, system-design-fundamentals, and the Data Engineering ecosystem for at-scale migrations—.

Resources

  • Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. Fowler insists that the strangler's goal is for the legacy to die —to be retired, not to coexist—: this lesson's done is the criterion that declares that death and authorizes the deletion. In English.
  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), ch. 3 — the migration's last mile: retiring (not just disconnecting) the functionality from the monolith, and why leaving it "just in case" perpetuates the problem. The source of why done is deleting. In English.
  • Neal Ford, Rebecca Parsons, and Patrick Kua, Building Evolutionary Architectures (O'Reilly, 2nd ed., 2022) — on defining objective and verifiable criteria for an architecture's state; done as a checklist of measurable conditions is that idea applied to the end of a migration. In English.
  • Martin Fowler, "Legacy Modernization" — martinfowler.com. The bliki with the entries on modernization and retirement of legacy systems that ground why "finished" means deleted and not turned off, and why the incremental and measured path beats the big-bang. In English.