Module 8: Project — Modernizing a Slice of Mercado

Project: modernize Mercado's catalog

Overview

You've reached the summit of the climb. The six previous lessons took you through each camp of the method —characterize (L2), put behind the facade (L3), extract with an ACL (L4), migrate the data (L5), measure the progress (L6), declare done (L7)—, each executed separately over the same slice. This project is the summit: chaining the six steps into a single program that runs the complete modernization of Mercado's catalog end to end, with its literal output phase by phase, ending in done = True and the legacy deleted. It's not a new technique; it's the whole method, executed in a single pass, so you see the complete film after having shot it scene by scene.

A capstone is distinguished from a lesson by its deliverable. Here you produce an artifact you can defend: a program —modernize_catalog— that runs the six phases in order over the same piece, printing the measure of each one (the number that says, without opinion, that that phase did its job): 6 cases green, fallback 100 that later closes, round-trip True, discrepancies 2→0, burn-down 1000→0, and done = True. Each measure comes from running the code, not from asserting it. When you finish, you won't have six loose techniques: you'll have a method you can apply to the next slice —payments, shipping, orders— until Mercado's monolith disappears.

And this project closes the whole guide. After the reference solution, the last section takes stock of the complete method and gives you the map of where to go next: the sister guides of the ecosystem that take the baton when the slice is already modernized. Here you finished learning the migration technique; there you learn what to build with it and where the piece you extracted ends up.

Connection with the module. This is the capstone deliverable: it integrates lessons 2 to 7 into a continuous execution over the catalog. Each phase of the program consumes what the previous one produced —phase 1's golden master closes phase 5's burn-down; phase 3's ACL protects phase 4's data migration—, and the measures chain together up to the done = True that authorizes deleting the legacy. Notice the capstone's boundary: here we integrate the modernization technique end to end. Where the extracted catalog ends up (microservice, events, API), the decision to modernize (the ADR), and at-scale data migrations belong to the sister guides —the last section of this lesson tells you which ones and in what order—.

The project statement

It's your turn to write the modernize_catalog program that runs the complete modernization of Mercado's catalog, chaining the six steps of the method over the same slice, end to end. The program must execute, in order, six phases, and each one must print its measure (the number that proves it did its job):

  1. Characterize (M2). Freeze the behavior of the legacy catalog in a golden master of 6 cases (quirks included) and catch a "clean" reimplementation that changes numbers. Measure: the golden master of 6 cases, the net in place.
  2. Facade (M3). Put a strangler router in front of the catalog and raise the traffic_percent from 0 to 100 with fallback. Measure: at 100%, the fallback at 100 (the bulk the modern doesn't yet cover).
  3. Extract (M5). Insert the ACL that translates old↔new and verify the round-trip. Measure: round_trip = True.
  4. Migrate data (M6). Run dual-write + backfill (with bugs), parallel-run (2 discrepancies), reconcile the cause (0 discrepancies), and do the read-switch. Measure: discrepancies 2→0.
  5. Measure (M7). Run the burn-down of legacy_calls to 0, closing the stubborn stretch (the bulk) with the modern implemented guided by phase 1's golden master. Measure: legacy_calls = 0, with the modern reproducing the 6 cases.
  6. Done (M7). Evaluate the done criterion (5 conditions: legacy_calls == 0, fallback == 0, discrepancies == 0, legacy_refs == 0, characterization green) and, if all are green, delete the legacy. Measure: done = True → DELETE.

The program must end in catalog_modernized = True and legacy_deleted = True, with a summary of one row per phase and its measure.

The rubric

Your deliverable is evaluated against these conditions —all verifiable by running the program, none of opinion—:

#CriterionHow it's verifiedPasses if
1The six phases run in orderThe phase-by-phase outputM2 → M3 → M5 → M6 → M7 (measure) → M7 (done) appear, in that order
2The characterization puts the net in placePhase 1Golden master of 6 cases; the "clean" reimplementation is caught
3The fallback reveals the bulk's gapPhase 2At traffic_percent=100, fallback=100
4The ACL is faithfulPhase 3round_trip = True
5The data is verified before the switchPhase 4Discrepancies 2 → 0; read=new only with 0
6The burn-down closes with the golden masterPhase 5legacy_calls = 0; the modern reproduces the golden's 6 cases
7Done is the conjunction of the 5 conditionsPhase 6done = True only with the five green
8It ends in the legacy deletedThe final summarycatalog_modernized = True and legacy_deleted = True
9The seams are respectedThe order and the dependenciesPhase 1's golden master closes phase 5's burn-down; phase 3's ACL protects phase 4

A program that runs the six phases but closes the burn-down with a "clean" bulk (not guided by the golden master) fails criterion 6 —the fallback wouldn't really reach zero—. One that declares done = True with the fallback still greater than zero fails criterion 7.

An analogy: the dress rehearsal with the whole cast

A theater production doesn't premiere the day each actor knows their role. It premieres after the dress rehearsal: the first time the whole cast runs the entire play, from start to finish, without stopping, with the real costumes, lights, music, and scene changes. Each actor already rehearsed their part separately —that was the work of the previous weeks—, but the dress rehearsal tests something no isolated rehearsal can: that the parts fit in sequence, that the exit of one scene is the entrance of the next, that the costume change can be made in the time the middle scene lasts. It's where you discover whether the play works as a whole, not just whether each piece works alone.

This project is the modernization's dress rehearsal. Each technique —characterize, facade, extract, migrate, measure, done— you already rehearsed separately in its lesson. Now you run the entire play in one pass, with the full cast, and you see the seams: the golden master you froze in the first scene reappears in the fifth to close the burn-down; the ACL of the third scene is what makes the fourth's data migration safe. The dress rehearsal's value isn't in the techniques (those you already know) but in seeing them fit in sequence, each exit being the entrance of the next, until the curtain falls with the legacy deleted.

Reference solution

Here's the complete modernize_catalog program, which runs the six phases over Mercado's catalog. Try writing it yourself —chaining the artifacts of lessons 2 to 7— before opening the solution.

See the reference solution (complete code)
# CAPSTONE: the complete modernization of Mercado's catalog in a SINGLE program.
# Chains the six steps of the method -characterize (M2), facade (M3), extract (M5),
# migrate data (M6), measure (M7), done (M7)- over the same slice, end to
# end, ending in done=True and the legacy DELETED. Each phase prints its measure.

import math
import zlib
from dataclasses import dataclass

TAX_RATE = 0.16
BULK_MIN_QTY = 10
BULK_DISCOUNT = 0.05


# ============================================================================
# The legacy catalog's calculation, with its quirks (bulk truncation, inactive
# charged the same). It's the source of truth the whole method preserves.
# ============================================================================
def legacy_price(unit_price_cents, quantity, active):
    subtotal = unit_price_cents * quantity
    if quantity >= BULK_MIN_QTY:
        subtotal = math.floor(subtotal * (1 - BULK_DISCOUNT) / 10) * 10   # quirk
    return math.floor(subtotal * (1 + TAX_RATE))                          # inactive: same


CASES = [
    ("ssd-1tb",   8999,  1, True),
    ("usb-hub",   3499, 10, True),
    ("kbd-mech",  7333, 12, True),
    ("mouse-pro", 2499,  3, True),
    ("cable-hdmi", 1299, 25, True),
    ("webcam-hd", 5999,  2, False),
]


# ---------------------------------------------------------------------------
def phase1_characterize():
    """M2: freeze the behavior in a golden master and catch a regression."""
    golden = {sku: legacy_price(p, q, a) for sku, p, q, a in CASES}

    def clean_price(p, q, a):                       # "clean" reimplementation
        if not a:
            return 0                                # breaks: stops charging inactives
        subtotal = p * q
        if q >= BULK_MIN_QTY:
            subtotal = round(subtotal * (1 - BULK_DISCOUNT))   # breaks: normal rounding
        return math.floor(subtotal * (1 + TAX_RATE))

    diffs = sum(1 for sku, p, q, a in CASES if clean_price(p, q, a) != golden[sku])
    print("PHASE 1 (M2) - characterize")
    print(f"  golden master: {len(golden)} frozen cases (quirks included)")
    print(f"  'clean' reimplementation compared: {diffs} regressions caught")
    print(f"  measure -> golden master of {len(golden)} cases (the net is in place)\n")
    return golden


# ---------------------------------------------------------------------------
def phase2_facade():
    """M3: strangler router, ramp 0->100. The fallback reveals the bulk's gap."""
    def legacy_catalog(r):
        return {"source": "legacy"}

    def modern_catalog(r):
        if r["quantity"] >= BULK_MIN_QTY:
            raise ValueError("modern: bulk not implemented")
        return {"source": "modern"}

    def bucket(i):
        return zlib.crc32(str(i).encode()) % 100

    reqs = [{"id": i, "quantity": 12 if i % 10 == 0 else 1} for i in range(1, 1001)]
    fallback_at_100 = 0
    for pct in (0, 10, 50, 100):
        c = {"modern_ok": 0, "fallback": 0, "legacy": 0}
        for r in reqs:
            if bucket(r["id"]) < pct:
                try:
                    modern_catalog(r); c["modern_ok"] += 1
                except Exception:
                    c["fallback"] += 1; legacy_catalog(r)
            else:
                c["legacy"] += 1; legacy_catalog(r)
        if pct == 100:
            fallback_at_100 = c["fallback"]
    print("PHASE 2 (M3) - facade")
    print("  strangler router: traffic_percent raised 0 -> 10 -> 50 -> 100")
    print(f"  at 100%: fallback = {fallback_at_100} (the uncovered volume purchases)")
    print(f"  measure -> traffic_percent=100, fallback={fallback_at_100} (bulk open)\n")
    return fallback_at_100


# ---------------------------------------------------------------------------
@dataclass
class Product:
    id: int
    name: str
    price_cents: int
    active: bool


def phase3_extract():
    """M5: ACL translates old<->new; the round-trip verifies the fidelity."""
    def to_modern(row):
        return Product(row["prod_id"], row["desc"], int(row["prc_cents"]), row["act"] == "Y")

    def to_legacy(p):
        return {"prod_id": p.id, "desc": p.name,
                "prc_cents": str(p.price_cents), "act": "Y" if p.active else "N"}

    rows = [
        {"prod_id": 1, "desc": "SSD 1TB",   "prc_cents": "8999", "act": "Y"},
        {"prod_id": 3, "desc": "Webcam HD", "prc_cents": "5999", "act": "N"},
    ]
    round_trip = all(to_legacy(to_modern(r)) == r for r in rows)
    print("PHASE 3 (M5) - extract")
    print("  ACL: to_modern (legacy->Product) and to_legacy (Product->legacy)")
    print(f"  round-trip to_legacy(to_modern(row)) == row for all: {round_trip}")
    print(f"  measure -> round_trip={round_trip} (the monolith receives its model intact)\n")
    return round_trip, to_modern, to_legacy


# ---------------------------------------------------------------------------
def phase4_migrate_data(to_modern):
    """M6: dual-write + backfill (with bugs) -> parallel_run 2 -> reconcile 0 -> switch."""
    old = {
        1: {"prod_id": 1, "desc": "SSD 1TB",   "prc_cents": "8999", "act": "Y"},
        2: {"prod_id": 2, "desc": "USB-C Hub", "prc_cents": "3499", "act": "Y"},
        3: {"prod_id": 3, "desc": "Webcam HD", "prc_cents": "5999", "act": "N"},
        4: {"prod_id": 4, "desc": "Mech Kbd",  "prc_cents": "7999", "act": "Y"},
    }
    new = {}

    def canon_old(r):
        return (r["prod_id"], r["desc"], int(r["prc_cents"]), r["act"] == "Y")

    def canon_new(p):
        return (p.id, p.name, p.price_cents, p.active)

    def backfill(skip_inactive, drop_digit):
        for pid, r in old.items():
            if skip_inactive and r["act"] == "N":
                continue
            price = int(r["prc_cents"]) // 10 if (drop_digit and pid == 4) else int(r["prc_cents"])
            cand = Product(r["prod_id"], r["desc"], price, r["act"] == "Y")
            if pid not in new or new[pid] != cand:
                new[pid] = cand

    def parallel_run():
        d = 0
        for pid, r in old.items():
            if pid not in new or canon_old(r) != canon_new(new[pid]):
                d += 1
        return d

    backfill(skip_inactive=True, drop_digit=True)      # with bugs
    disc_before = parallel_run()
    backfill(skip_inactive=False, drop_digit=False)    # reconcile the cause
    disc_after = parallel_run()
    read = "new" if disc_after == 0 else "old"
    print("PHASE 4 (M6) - migrate data")
    print(f"  parallel_run after buggy backfill: {disc_before} discrepancies")
    print(f"  reconcile the cause + re-backfill: {disc_after} discrepancies")
    print(f"  read_switch allowed (disc==0): read={read}")
    print(f"  measure -> discrepancies {disc_before} -> {disc_after}, read={read}\n")
    return disc_after


# ---------------------------------------------------------------------------
def phase5_measure(golden):
    """M7: burn-down of legacy_calls to 0. The stubborn stretch (bulk) closes guided by
    the golden master; the final modern reproduces the 6 cases (characterization green)."""
    def final_modern_price(p, q, active):              # the complete modern, guided by the golden
        subtotal = p * q
        if q >= BULK_MIN_QTY:
            subtotal = math.floor(subtotal * (1 - BULK_DISCOUNT) / 10) * 10   # faithful bulk
        return math.floor(subtotal * (1 + TAX_RATE))                          # inactive: same

    green_cases = sum(1 for sku, p, q, a in CASES if final_modern_price(p, q, a) == golden[sku])
    bulk_matches = green_cases == len(CASES)
    burn_down = [1000, 600, 300, 100, 100, 0] if bulk_matches else [1000, 600, 300, 100, 100, 100]
    legacy_calls = burn_down[-1]
    print("PHASE 5 (M7) - measure")
    print(f"  burn-down of legacy_calls: {' -> '.join(map(str, burn_down))}")
    print(f"  the final modern reproduces the golden master: {green_cases}/{len(CASES)} cases")
    print(f"  measure -> legacy_calls={legacy_calls} (the stubborn stretch closed with the golden)\n")
    return legacy_calls, green_cases


# ---------------------------------------------------------------------------
def phase6_done(green_cases, fallback, round_trip, discrepancies, legacy_calls):
    """M7: done as a list of 5 conditions -> delete the legacy."""
    checks = {
        "legacy_calls == 0":      legacy_calls == 0,
        "fallback == 0":          fallback == 0,
        "discrepancies == 0":     discrepancies == 0,
        "legacy_refs == 0":       True,                 # the fitness reached 0 (measured in M7)
        "characterization green": green_cases == len(CASES),
    }
    is_done = all(checks.values())
    print("PHASE 6 (M7) - done")
    for name, ok in checks.items():
        print(f"  [{'x' if ok else ' '}] {name}")
    print(f"  measure -> done={is_done} -> {'DELETE the legacy' if is_done else 'conditions missing'}\n")
    return is_done


# ============================================================================
# The complete method, chained over Mercado's catalog.
# ============================================================================
print("Modernization of Mercado's catalog: the complete method, end to end\n")
print("=" * 76)
golden = phase1_characterize()
fallback = phase2_facade()
round_trip, to_modern, to_legacy = phase3_extract()
discrepancies = phase4_migrate_data(to_modern)
legacy_calls, green_cases = phase5_measure(golden)
# the bulk's fallback closes in phase 5 (the modern now covers the bulk) -> 0
done = phase6_done(green_cases, 0, round_trip, discrepancies, legacy_calls)
print("=" * 76)

print("\nSummary: one row per phase, each 'measure' is a number that came from running the code")
print(f"{'mod':>3}  {'step':<14}{'measure'}")
print("-" * 60)
print(f"{'M2':>3}  {'characterize':<14}6 cases green")
print(f"{'M3':>3}  {'facade':<14}fallback 100 (bulk open)")
print(f"{'M5':>3}  {'extract':<14}round-trip: {round_trip}")
print(f"{'M6':>3}  {'migrate data':<14}discrepancies 2 -> 0")
print(f"{'M7':>3}  {'measure':<14}burn-down 1000 -> 0 (bulk migrated)")
print(f"{'M7':>3}  {'done':<14}done = {done}")
print("-" * 60)
print(f"\n  catalog_modernized = {done}   legacy_deleted = {done}")
print("  From 'legacy function nobody touches' to 'service with the legacy deleted',")
print("  with Mercado alive at each step and every claim measured. That's the method.")

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

Modernization of Mercado's catalog: the complete method, end to end

============================================================================
PHASE 1 (M2) - characterize
  golden master: 6 frozen cases (quirks included)
  'clean' reimplementation compared: 3 regressions caught
  measure -> golden master of 6 cases (the net is in place)

PHASE 2 (M3) - facade
  strangler router: traffic_percent raised 0 -> 10 -> 50 -> 100
  at 100%: fallback = 100 (the uncovered volume purchases)
  measure -> traffic_percent=100, fallback=100 (bulk open)

PHASE 3 (M5) - extract
  ACL: to_modern (legacy->Product) and to_legacy (Product->legacy)
  round-trip to_legacy(to_modern(row)) == row for all: True
  measure -> round_trip=True (the monolith receives its model intact)

PHASE 4 (M6) - migrate data
  parallel_run after buggy backfill: 2 discrepancies
  reconcile the cause + re-backfill: 0 discrepancies
  read_switch allowed (disc==0): read=new
  measure -> discrepancies 2 -> 0, read=new

PHASE 5 (M7) - measure
  burn-down of legacy_calls: 1000 -> 600 -> 300 -> 100 -> 100 -> 0
  the final modern reproduces the golden master: 6/6 cases
  measure -> legacy_calls=0 (the stubborn stretch closed with the golden)

PHASE 6 (M7) - done
  [x] legacy_calls == 0
  [x] fallback == 0
  [x] discrepancies == 0
  [x] legacy_refs == 0
  [x] characterization green
  measure -> done=True -> DELETE the legacy

============================================================================

Summary: one row per phase, each 'measure' is a number that came from running the code
mod  step          measure
------------------------------------------------------------
 M2  characterize  6 cases green
 M3  facade        fallback 100 (bulk open)
 M5  extract       round-trip: True
 M6  migrate data  discrepancies 2 -> 0
 M7  measure       burn-down 1000 -> 0 (bulk migrated)
 M7  done          done = True
------------------------------------------------------------

  catalog_modernized = True   legacy_deleted = True
  From 'legacy function nobody touches' to 'service with the legacy deleted',
  with Mercado alive at each step and every claim measured. That's the method.

Read the output from top to bottom, because it's the whole method running in a single pass, and each phase hands its measure to the next.

Phase 1 (M2) — characterize. The golden master freezes 6 cases of the legacy catalog with their quirks, and the "clean" reimplementation is caught with 3 regressions. The net is in place: any later change is compared against these 6 numbers. This phase produces the golden that phase 5 will need.

Phase 2 (M3) — facade. The strangler router raises the traffic_percent from 0 to 100, and at 100% the fallback marks 100 —the volume purchases the modern doesn't yet cover—. The measure says, without drama, that "the traffic is on the new" isn't "finished": there's a gap (the bulk) the legacy still covers.

Phase 3 (M5) — extract. The ACL translates old↔new, and the round-trip gives True: the monolith receives its model intact whatever happens inside. This phase produces the ACL that phase 4 needs to migrate the data without breaking the monolith's contract.

Phase 4 (M6) — migrate data. The buggy backfill leaves 2 discrepancies, the parallel-run catches them before the switch, the reconciliation fixes the cause and brings them to 0, and only then read=new. The data moved verified, with phase 3's ACL protecting the monolith during the move.

Phase 5 (M7) — measure. The burn-down drops 1000 → 600 → 300 → 100 → 100 → 0. Notice the stubborn stretch: it gets stuck at 100 (the bulk) and only closes when the modern implements the bulk guided by phase 1's golden master —the final modern reproduces the 6/6 cases, so the fallback reaches zero and legacy_calls = 0—. Here you see the seam: step 1's golden master is what allows finishing step 5.

Phase 6 (M7) — done. The five conditions are green —legacy_calls == 0, fallback == 0, discrepancies == 0, legacy_refs == 0, characterization green—, so done = True and the legacy is deleted. The final summary seals it: catalog_modernized = True, legacy_deleted = True. The slice went from "legacy function nobody touches" to "service with the legacy deleted", with Mercado alive at each step and every claim measured.

The six-row summary is the method in its most compressed form: six phases, six measures, each one a number that came from running the code. And the seams are on display —M2's golden master closing M7's burn-down, M5's ACL protecting M6's migration—. That's what the capstone reveals and an isolated lesson can't: not six techniques, but a method where each link holds up the next.

Transfer exercises

Exercise 1 — Break a seam. Suppose that in phase 5 the modern implements the bulk in a "clean" way (5% with normal rounding), not guided by the golden master. (a) What would phase 5 measure differently? (b) How would that affect phase 6? (c) What does this tell you about the dependency between phase 1 and phase 5?

See solution

(a) Phase 5 would measure that the final modern reproduces fewer than 6/6 cases of the golden master (it would fail on the volume cases where the truncation and the rounding differ, like kbd-mech and cable-hdmi). Since green_cases < 6, bulk_matches would be False, and the burn-down wouldn't reach zero: it would stay at 1000 → 600 → 300 → 100 → 100 → 100, stuck in the stubborn stretch. legacy_calls would be 100, not 0.

(b) Phase 6 would fail: with legacy_calls = 100, the condition legacy_calls == 0 would be unchecked, and the condition characterization green (which requires green_cases == 6) too. done would be False, the legacy could not be deleted, and catalog_modernized would be False. The migration would stay at "98.5%" —the eternal migration—.

(c) That phase 5 depends on phase 1: the burn-down only closes if the modern reproduces the legacy's exact behavior, and that's only achieved by guiding yourself with the golden master phase 1 froze. Without the net of step 1, step 5 can't finish. It's the method's most important seam: the characterization at the start is literally what allows reaching the end. Breaking it (implementing the "clean" bulk) breaks the completion of the whole migration.

Exercise 2 — The order matters. The program runs the phases in the order M2 → M3 → M5 → M6 → M7 → M7. (a) Why does characterizing (phase 1) go before everything? (b) Why does extracting with an ACL (phase 3) go before migrating the data (phase 4)? (c) What would happen if you measured (phase 5) before putting in the facade (phase 2)?

See solution

(a) Characterizing goes first because it's the safety net that catches the regressions of everything else. Phase 1's golden master is what verifies that every later reimplementation (including the modern's bulk in phase 5) preserves the behavior. Without it, any change could alter a price without anyone noticing. The net is put in place before climbing, not after.

(b) Phase 3's ACL goes before migrating the data (phase 4) because it's what keeps the monolith identical during the move. When the data moves from shared_db to owned_db (the source changes underneath), the return ACL is what keeps delivering to the monolith its exact old model. Without the ACL put in place first, moving the data would break the monolith's contract —the hundreds of consumers that expect the old model—.

(c) Measuring (phase 5) before putting in the facade (phase 2) wouldn't make sense: the burn-down measures how many calls to the legacy are left and how the fallback drops toward zero, but there's nothing to measure if the facade isn't diverting traffic yet. The measurement stands on the movement that the facade (and the extraction, and the data migration) produces; measuring a movement that didn't start would give zero movement, not progress. That's why measuring goes at the end: you only measure a movement in progress.

Exercise 3 — The next slice. You finished the catalog. Now it's your turn for orders, which depends on catalog (already extracted) and has nightly batch processes. (a) What of the method do you reuse as is? (b) What would you adjust for orders? (c) Why did modernizing catalog first make it easier to modernize orders?

See solution

(a) You reuse the complete method as is: the six steps in the same order (characterize → facade → extract with an ACL → migrate data → measure → done), with the same techniques and the same artifacts (golden master, strangler router, ACL, parallel-run, burn-down, done criterion). The method is reusable by design —that's the reason for learning it as a method and not as six loose tricks—.

(b) You'd adjust the done criterion and the scope for orders's particularities: you'd add conditions for the nightly batch processes (repointed to the modern and verified in at least one cycle), because orders has flows that run outside the normal traffic and are easy to forget in a legacy_calls measured only over the synchronous traffic. And you'd tackle those batch flows early (not at the end), because they're orders's probable stubborn stretch. The characterization of orders would freeze its quirks (the order ones, not the price ones).

(c) Because orders depends on catalog, and catalog is already extracted: when orders needs product data, it asks the extracted catalog service (with its clean model and its API), not a function tangled inside the monolith. One of the dependencies that would have complicated the extraction of orders is already resolved. This is the payoff of having started with the leaf (lesson 2): each modernized slice smooths the path for the ones that depend on it. The monolith is undone from the outside in, and each step leaves the next one easier.

The close of the whole guide: the method, and where to go next

You finished the guide. It's worth stopping a moment and seeing what you learned, because it's more than a list of techniques.

You started with an uncomfortable conviction (module 1): the big rewrite almost always fails, and the path that works is the incremental, measured, and reversible one. Everything else was how to make that path real. You learned to touch scary code by putting a net under it (characterization tests, M2); to divert traffic from the old to the new without a big-bang (strangler fig, M3); to migrate the implementation from within when there's no external boundary (branch by abstraction, M4); to extract a service with its own model and its own data without breaking the monolith (anti-corruption layer, M5); to move the data without turning off the system (dual-write, backfill, parallel-run, M6); and to measure the progress to know whether you're advancing and when you finished (burn-down, fitness function, done, M7). And in this capstone (M8) you chained them all into a method —six steps where each link holds up the next— and ran it end to end over Mercado's catalog, until deleting the legacy.

The figure that sums up why this path wins you saw in lesson 7: the incremental method caught 105 problems before production that a big-bang would have sent without verification. It's not an aesthetic preference for the incremental; it's that measuring each step, with the business alive and every move reversible, catches the errors before they touch a customer. That's the migration technique this guide gave you.

And here's the boundary —which is also the map of where to go next—. This guide taught how to bring a piece of the old system to the new safely. It didn't teach where that piece ends up or what to build with it. For that, the sister guides of the ecosystem take the baton:

  • architecture-decisions-and-tradeoffs — The decision to modernize: the ADR that records why to migrate, the cost, reversibility as a design property, and the fitness function as a general concept. This guide executed the how; there you learn to decide and record the why.
  • architectural-styles-and-boundariesWhere the extracted service ends up: modular monolith, microservices, and how the boundaries (the bounded contexts) that decided what a "slice" was are drawn. The extracted catalog is now a service; that guide teaches what shape to give it.
  • event-driven-architecture — Whether the extracted catalog should communicate through events instead of direct calls: queues, publish/subscribe, eventual consistency. The event-oriented destination of the piece you migrated.
  • system-design-fundamentals — The design of the system the piece arrives at: scaling, caching, load balancing, the infrastructure decisions that surround an already-extracted service.
  • Data Engineering ecosystem — Migrating data at real scale in production: CDC (change data capture), pipelines of billions of rows, automated reconciliation tools. Here you executed the idea (read from both, compare, report) over five rows; there you learn the tools that scale it to production.

The order that makes sense: record the decision (architecture-decisions) before modernizing; decide the destination shape (architectural-styles, event-driven, system-design) to know where you're taking the piece; and use Data Engineering when the data migration stops being of five rows and becomes billions. With this guide's technique in your hands, each of those guides tells you what to build with it.

Summary and next step

In this capstone you integrated the six steps of the method into a single executed program, modernize_catalog, which ran the complete modernization of Mercado's catalog end to end: characterize (golden master of 6 cases, the net in place), facade (fallback 100 revealing the bulk's gap), extract (round-trip True), migrate data (discrepancies 2→0), measure (burn-down 1000→0, the stubborn stretch closed guided by the golden master), and done (the 5 conditions green → delete the legacy). You saw, with the dress rehearsal that runs the whole play with the full cast, that the capstone's value isn't in the techniques but in the seams —step 1's golden master closing step 5's burn-down, step 3's ACL protecting step 4's migration—. And you ended in catalog_modernized = True, legacy_deleted = True: the slice went from legacy function nobody touched to service with the legacy deleted, with Mercado alive at each step and every claim measured.

With this you close the legacy modernization and migration guide. You have a method —not six loose techniques— that you can apply to Mercado's next slice, and the next, until the monolith disappears. You know how to take a scary legacy system and modernize a part safely, without turning off the business, without a big-bang, measuring each step until deleting the old. And you know why that path —the incremental, measured, and reversible one— beats the rewrite: not on faith, but on the problems it catches before they touch a customer.

The next step is outside this guide, in the sister ones the previous section listed: record the decision to modernize (architecture-decisions-and-tradeoffs), design the destination shape of the piece you extracted (architectural-styles-and-boundaries, event-driven-architecture, system-design-fundamentals), and scale the data migration when it stops being of five rows (Data Engineering). You learned the technique of bringing the old system to the new; now go decide where, and build what.

Resources

  • Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004) — the book that founds the method's first step (characterize before changing) and holds up the whole arc: modernizing with a safety net. The go-to reading for taking this capstone to a real system. In English.
  • Sam Newman, Monolith to Microservices (O'Reilly, 2019) — the comprehensive reference for the complete method: extracting services, migrating data, and doing it incrementally and measured. The book that comes closest to describing this guide's entire method. In English.
  • Martin Fowler, "StranglerFigApplication" and "BranchByAbstraction" — martinfowler.com/bliki/StranglerFigApplication.html; and Sam Newman's parallel run (Monolith to Microservices). The patterns that hold up the method's central steps: diverting traffic incrementally, migrating from within without a long-lived branch, and comparing old and new before trusting. In English.
  • Chris Richardson, "Microservice Architecture" — microservices.io. The catalog of decomposition and migration patterns; useful as an index to see, from above, how the pieces the method chains together fit, and as a bridge toward the destination guides (styles, events). In English.