Module 8: Project — Modernizing a Slice of Mercado
Choose and characterize the slice
Overview
The method begins here, and it begins as it should: before touching anything. Lesson 1 gave you the map of the ascent; this one takes the first step, which integrates two modules —module 1 (why modernize by slices) and module 2 (characterize the legacy)— into a single decision and a single safety net. First you choose which slice to modernize and why the catalog is the convenient one to do first. Then, and this is what no prudent team skips, you freeze its current behavior in a golden master —quirks included— so that anything you do afterward is compared against a snapshot of the "before" and you can't break something without noticing.
Modernizing by slices, not all at once, was module 1's lesson: the big rewrite fails, and the path that works is incremental. But "incremental" immediately raises a question: which slice first? The answer isn't random. You choose the slice with the fewest dependencies toward the rest of the monolith —the "leaf" of the dependency graph, the module that least needs the others—, because it's the one you can extract with the fewest threads to cut. In Mercado, that's the catalog: orders depends on catalog (it needs to know which products exist and how much they cost), payments depends on orders, shipping depends on orders —but catalog depends on none—. It's the leaf. Starting there is starting with what hurts least.
With the slice chosen, comes the net. The catalog calculates prices, and that calculation has quirks —strange behaviors that have been in production for years and that, without you knowing, someone depends on—. Before reimplementing a single line, you record a golden master: you run the legacy catalog over a set of cases, save its exact outputs (quirks and all), and that record becomes the canonical reference of "how it works today". Any candidate reimplementation is compared against it in a parallel-run, and the diff tells you, without you having to guess, where you changed the behavior. As you're going to see, a "clean" reimplementation —well-intentioned, that "fixes" what looked like a bug— changes numbers that were fine, and the golden master catches it.
Connection with the module. This is the method's first technique (M1 + M2), and it produces the artifact all the following lessons depend on: the golden master. Lesson 3 will use it as reference when the strangler router diverts the traffic (the modern is compared against it); lesson 6 will use it to close the last stretch of the burn-down (the modern implements the volume discount guided by the golden master); and lesson 7 will put it as one of the done conditions (the characterization green). Notice the boundary: here we use the characterization test as a migration tool —freezing the behavior to be able to change safely—, not as testing theory; the mechanics of the golden master, sampling, and approval testing in depth belong to the Testing ecosystem. Here it's the safety net of the first step.
An analogy: photographing the house before the remodel
When an insurer is going to cover a remodel, it does something before the first mason touches a wall: it photographs the whole house, room by room. Not to admire it —to have an objective record of the "before state"—. If at the end of the work a crack appears in the living room, the question "was this crack already there?" isn't resolved by anyone's memory (which is interested and blurry): it's resolved by comparing against the photos. If the crack is in the photo, it already existed; if it's not, the work caused it. The photos turn a discussion of opinions into a comparison of facts.
And notice a crucial detail of those photos: they record the house as it is, imperfections included. If the living room already had a damp stain on the ceiling, the photo captures it. The insurer doesn't "fix" the stain in the photo or omit it "because it's ugly"; it records it as-is, because the point isn't for the house to be perfect, but to have a faithful record of the real state. If a mason, with good intentions, covered that stain believing it's a defect, and it turns out the stain marked a leak the owner was watching, "fixing" it without warning would be a problem —it changed something someone depended on—.
The golden master is that photograph of the house. You record the output of the legacy catalog for a set of cases —that's your snapshot of the "before", quirks and all—. And when you reimplement the calculation, you compare against the photo: if a number changed, you know it, and you know exactly which one. The legacy's quirks are the damp stains: they look like defects, but the golden master records them faithfully, because maybe someone —the checkout, a partner, a reconciliation— depends on that exact number. You photograph before touching, so that no new crack passes for an old one.
Worked example: the catalog's golden master and the caught regression
We're going to execute the complete first step. Mercado's catalog calculates prices with two real quirks: (1) the volume discount (from 10 units) truncates the subtotal to the lowest ten-cent —an inheritance from an old system that only handled prices in tens—, and (2) the inactive product is charged the same —the legacy never looks at the active field when calculating the price, and the checkout depends on that for items already in the cart—. We record the golden master of 6 cases and run against it a "clean" reimplementation that, with all the good intentions, rounds the bulk normally and doesn't charge the inactives. The golden master catches what the cleanup broke.
# Step 1 of the method: characterize the catalog BEFORE touching it. The golden master
# freezes the legacy's current behavior -quirks included- and catches the
# regression when a "clean" reimplementation changes the number.
import math
TAX_RATE = 0.16
BULK_MIN_QTY = 10
BULK_DISCOUNT = 0.05
def legacy_price(unit_price_cents, quantity, active):
"""The legacy catalog's calculation, with its TWO quirks."""
subtotal = unit_price_cents * quantity
# QUIRK 1: the volume discount TRUNCATES to the lowest ten-cent
# (an inheritance from an old system that only handled prices in tens).
if quantity >= BULK_MIN_QTY:
discounted = subtotal * (1 - BULK_DISCOUNT)
subtotal = math.floor(discounted / 10) * 10
total = math.floor(subtotal * (1 + TAX_RATE))
# QUIRK 2: the inactive product is charged the SAME (the legacy never looks at 'active'
# for the price; the checkout depends on this for items already in the cart).
return total
def clean_price(unit_price_cents, quantity, active):
"""'Clean' reimplementation: rounds the bulk normally and doesn't charge inactives."""
if not active:
return 0 # "cleanup": don't charge inactives
subtotal = unit_price_cents * quantity
if quantity >= BULK_MIN_QTY:
subtotal = round(subtotal * (1 - BULK_DISCOUNT)) # "cleanup": normal rounding
return math.floor(subtotal * (1 + TAX_RATE))
# --- The golden master: 6 cases that sweep the catalog's quirks. ---
cases = [
# (sku, unit_price_cents, quantity, active)
("ssd-1tb", 8999, 1, True), # normal
("usb-hub", 3499, 10, True), # bulk: triggers the truncation quirk
("kbd-mech", 7333, 12, True), # bulk with "dirty" numbers
("mouse-pro", 2499, 3, True), # normal
("cable-hdmi", 1299, 25, True), # large bulk
("webcam-hd", 5999, 2, False), # INACTIVE: the legacy charges it the same
]
golden_master = [(sku, legacy_price(p, q, a)) for sku, p, q, a in cases]
print("Golden master of the legacy catalog (6 cases, price in cents)\n")
for sku, price in golden_master:
print(f" {sku:<11} legacy_price = {price}")
print()
# --- Parallel-run: the "clean" reimplementation against the golden master. ---
print("parallel_run: clean_price vs golden_master")
print(f" {'sku':<11}{'golden':>8}{'clean':>8}{'diff?':>7}")
print(" " + "-" * 34)
diffs = []
for (sku, expected), (_, p, q, a) in zip(golden_master, cases):
actual = clean_price(p, q, a)
mark = "OK" if actual == expected else "DIFF"
if actual != expected:
diffs.append((sku, expected, actual))
print(f" {sku:<11}{expected:>8}{actual:>8}{mark:>7}")
print(" " + "-" * 34)
print(f"\n {len(cases) - len(diffs)} match, {len(diffs)} differ.")
for sku, expected, actual in diffs:
print(f" REGRESSION in {sku}: legacy={expected}, clean={actual}")
print("\n The golden master caught the regression: the 'clean' reimplementation changed")
print(" the number in the bulk (rounding) and in the inactive (stopped charging it). Before")
print(" touching the catalog, its behavior -quirks included- was frozen.")
What to expect. When you run the file, the output is exactly this:
Golden master of the legacy catalog (6 cases, price in cents)
ssd-1tb legacy_price = 10438
usb-hub legacy_price = 38558
kbd-mech legacy_price = 96964
mouse-pro legacy_price = 8696
cable-hdmi legacy_price = 35786
webcam-hd legacy_price = 13917
parallel_run: clean_price vs golden_master
sku golden clean diff?
----------------------------------
ssd-1tb 10438 10438 OK
usb-hub 38558 38558 OK
kbd-mech 96964 96971 DIFF
mouse-pro 8696 8696 OK
cable-hdmi 35786 35787 DIFF
webcam-hd 13917 0 DIFF
----------------------------------
3 match, 3 differ.
REGRESSION in kbd-mech: legacy=96964, clean=96971
REGRESSION in cable-hdmi: legacy=35786, clean=35787
REGRESSION in webcam-hd: legacy=13917, clean=0
The golden master caught the regression: the 'clean' reimplementation changed
the number in the bulk (rounding) and in the inactive (stopped charging it). Before
touching the catalog, its behavior -quirks included- was frozen.
Read the result in two parts, because together they tell the method's first step.
The photo: the golden master. The six numbers above are the photograph of the legacy catalog —the "how it works today", in cents, quirks included—. The ssd-1tb costs 10438 (a normal product with tax), the usb-hub in a quantity of 10 costs 38558 (with the volume discount and its strange truncation), and the webcam-hd, even though it's inactive, costs 13917 —the legacy charges it the same—. We don't judge whether these numbers are "correct"; we only record them as the reference. They're the photos of the house before the work.
The comparison: the parallel-run. The "clean" reimplementation runs over the same cases and is compared against the photo. Of the 6 cases, 3 match and 3 differ —and the 3 differences are exactly the two quirks the cleanup "fixed"—:
webcam-hd: 13917 → 0. The clean reimplementation decided that an inactive product shouldn't be charged and returns 0. It sounds reasonable —"why charge something inactive?"—, but it changed the behavior the checkout depends on: an inactive item that's already in a customer's cart should be charged at the usual price, not become free. The "cleanup" just gave away products. The golden master caught it immediately: 13917 against 0, a difference impossible to ignore.kbd-mech: 96964 → 96971 andcable-hdmi: 35786 → 35787. Both are volume-discount cases, and they differ by a few cents because the reimplementation rounded the bulk normally instead of truncating to the ten-cent as the legacy does. They look like tiny differences —seven cents, one cent—, but they're a real price change for the customer, and if a loyalty partner reconciles against those exact numbers, the difference breaks the reconciliation.
And notice a fine detail: usb-hub matches (38558 in both), even though it's also a volume case. The legacy's truncation and the candidate's rounding gave, by chance, the same number for that case. This is important: the bulk quirk doesn't manifest in all volume cases, only in some, depending on how the cents fall. If you had characterized by hand and by luck had only tested usb-hub, the reimplementation would have passed your test green and the regression would have gone to production. The golden master, with its several volume cases, doesn't leave you that margin of luck.
The method's first step is complete: you chose the slice (catalog, the leaf of the seam) and froze its behavior in a golden master that already proved its value by catching a regression before you touched the production code. The net is in place. Now you can climb.
Deep dive: why the leaf first, and why the quirk is frozen
Two decisions of this step deserve development, because they're the ones that hold up everything that follows.
Why the catalog first: the leaf of the seam. When you decompose a monolith, the extraction order matters, and the rule is to start with the module with the fewest outbound dependencies —the one that least needs the others—. The reason is mechanical: extracting a module means cutting the threads that join it to the rest, and a module that depends on many others has many threads to cut (and each cut is risk). The catalog is the leaf because the others depend on it, not the other way around:
payments ──> orders ──> catalog
│ ▲
shipping ──────┘ │
(catalog depends on no one)
OUTBOUND dependencies per module:
catalog : 0 <- the leaf: extracted with fewer threads to cut
orders : 1 (depends on catalog)
shipping : 1 (depends on orders)
payments : 1 (depends on orders)
Extracting catalog first doesn't force extracting anything else before: since it doesn't depend on other modules, taking it out drags no dependencies. If you started with orders, you'd have to deal with its dependency on catalog from day one —more threads, more risk, in the step where you have the least experience—. The leaf first is module 5's rule: you start where there's least to cut, gain experience with the pattern, and advance toward the more tangled modules with the method already practiced.
Why the quirk is frozen instead of "fixed". The most natural temptation when reimplementing old code is to clean it up: "this strange truncation is clearly a bug, I'll do it right"; "charging an inactive product makes no sense, I'll remove it". The golden master exists to stop that temptation cold, because a behavior that's been in production for years almost never stands alone: with enough consumers, every observable behavior becomes a contract someone depends on, even if nobody wrote it. The bulk truncation could be exactly the number a loyalty partner reconciles its rebate against; charging the inactive could be what keeps a cart with a just-deactivated item from breaking at checkout. The first step's rule is hard and clear: characterizing preserves the behavior, it doesn't improve it. First you freeze what's there —apparent bug included—, and afterward, if you really want to change a quirk, you do it as a deliberate, reviewed, and approved behavior change (re-recording the golden master consciously), not as a silent "cleanup" slipped into a reimplementation. The photo is taken before touching; improving comes after, and with permission.
Common mistakes
Starting with the most tangled slice "because it's the most important". What happens: the team decides to modernize payments or orders first, because they're the heart of the business, and runs into all their dependencies toward the rest of the monolith from day one. Why it happens: "the most important first" sounds like good prioritization, and business importance is confused with extraction order. How to spot it: the first extraction attempt gets stuck on dependencies —to take out orders you have to deal with catalog, payments, and shipping at once—, and the team gets discouraged before completing a single slice. How to fix it: start with the leaf of the seam —the module with the fewest outbound dependencies, the catalog—, not the most important one. The first extraction is where you learn the method at the lowest risk; spending that first time on the most tangled module is learning to swim in the deep end. Business importance decides what gets modernized eventually; the dependencies decide in what order.
"Cleaning up" the quirks when reimplementing, without recording them first. What happens: when rewriting the catalog, the developer "fixes" what looks like a bug (the strange truncation, charging the inactive) believing they're improving the code. Why it happens: the quirks look like defects, and cleaning ugly code feels like the right thing. How to spot it: without a golden master, the change goes unnoticed until a consumer complains —the partner whose reconciliation broke, the checkout that started giving away products—. How to fix it: record the golden master before touching a line, with the quirks included, and compare every reimplementation against it. The golden master turns "I fixed a bug" into "I changed three numbers, here they are": if the change was wanted, you approve it consciously; if not —like the inactive that became free—, you revert it. The quirk isn't cleaned up on instinct; it's frozen first and changed after, with permission.
Characterizing by hand with few cases and believing it's enough. What happens: the team tests three or four cases chosen by hand, they all pass, and it considers the characterization good. Why it happens: a few cases are quick to write and give a feeling of coverage. How to spot it: the quirk lives in combinations that weren't tested —like usb-hub, a volume case that matches even though other volume cases differ—, so a hand test can land right on the cases that don't reveal the problem. How to fix it: use several cases that sweep the quirks on purpose (several volume cases, not one; the inactive case explicit), because the quirks don't manifest in all cases, only in some. In the example, testing only usb-hub would have given a false green; it was kbd-mech and cable-hdmi that revealed the truncation. Covering the quirks with enough volume is what makes the golden master catch what intuition lets pass.
Exercises
Exercise 1 — Why the catalog first? (a) What does it mean that the catalog is the "leaf" of the dependency graph? (b) What problem would appear if you started the modernization with orders? (c) What criterion decides the order of extraction, and how does it differ from the criterion that decides what gets modernized eventually?
See solution
(a) That the catalog has zero outbound dependencies: it needs no other module of the monolith to work (orders, payments, and shipping depend on it, but it depends on none). In the dependency graph it's a leaf —a node with arrows coming into it, but none going out to others—. That's why extracting it doesn't force cutting threads toward other modules: it's the one that ties the least.
(b) Starting with orders would face you, from day one, with its dependency on catalog (and those of payments and shipping that depend on orders). You'd have to decide what to do with those threads —extract catalog too? put an ACL toward it?— in the step where you have the least experience with the method. More threads to cut means more risk and more complexity in the first extraction, right where it's convenient for it to be simple.
(c) The order of extraction is decided by the dependencies: you start with the leaf (fewest outbound dependencies) and advance toward the more tangled modules with the method already practiced. What gets modernized eventually is decided by business importance (the whole monolith, in the long run). The two criteria are different: payments may be the most important, but that doesn't make it first —first is what's extracted with the least risk, to learn the pattern before applying it to the tangled ones—.
Exercise 2 — The cleanup that breaks. The "clean" reimplementation changed three numbers relative to the golden master. (a) Which of the three differences is the most dangerous for the business and why? (b) Why did usb-hub match even though it's also a volume case? (c) If a developer insists that the bulk truncation "is a bug and needs to be fixed", what's the correct procedure?
See solution
(a) The most dangerous is webcam-hd: 13917 → 0. The bulk differences are of cents (important for a reconciliation, but bounded); the inactive that becomes free is a big and direct behavior change: any inactive product a customer has in the cart would drop to cost 0, giving away inventory. It's the kind of "clean improvement" that sounds reasonable in the abstract but breaks a real contract —the checkout depends on inactives already in the cart being charged at the usual price—.
(b) Because the bulk quirk (truncating to the ten-cent vs. rounding normally) doesn't manifest in all volume cases, only in those where the cents fall in a way that the truncation and the rounding differ. For usb-hub, by chance, both methods gave the same number (38558). It's exactly the kind of coincidence that makes the regression slippery: testing one volume case could land right on one that matches, giving a false green.
(c) The correct procedure: first freeze, then change with permission. Don't "fix" the truncation by slipping it into a reimplementation. If the business really decides that the bulk should now round normally, that's a deliberate behavior change: it's documented, reviewed, approved, and the golden master is re-recorded consciously to the new behavior —reading the diff, which is the literal list of which prices change and for whom—. What you don't do is change the number silently believing you're "improving" code; that's the first step's hard rule (characterizing preserves, doesn't improve).
Exercise 3 — Design the net for another slice. You're the one to modernize Mercado's shipping module, which calculates shipping costs with its own quirks (a weight rounding up, a minimum fee applied strangely). (a) What would you record in the golden master and how? (b) What cases would you make sure to include? (c) How would you use the golden master when you reimplement the calculation?
See solution
(a) I'd record the output of the legacy shipping for a set of cases: for each case (weight, destination, service type, order value), I'd run the legacy calculation and save the exact shipping cost it returns —quirks included—. That record (cases → costs) is the golden master, and I'd store it versioned alongside the code, as the shipping's "before" photo.
(b) I'd make sure to include cases that sweep the quirks on purpose: several weights right around the rounding thresholds (to expose the rounding-up in the cases where it matters), several orders below and above the minimum-fee amount (to freeze how that "strange" fee is applied), and all the service types and several destinations. As in the catalog, the quirk doesn't manifest in all cases, so a single case per quirk isn't enough —I need volume around the edges—.
(c) When reimplementing the calculation, I'd run the new version over the same cases and compare it against the golden master in a parallel-run. If they all match, the behavior was preserved (green). If something differs, the diff would tell me exactly which case changed and how —and there I'd decide: if the change was wanted (the business changed a fee), I re-record the golden master consciously; if not (I cleaned up a quirk someone depends on), I revert my reimplementation—. The golden master is the net that catches the regression before it reaches a customer.
Summary and next step
In this lesson you took the method's first step, which integrates module 1 (modernize by slices) and module 2 (characterize the legacy). You chose the slice: the catalog, the leaf of the seam —the module with zero outbound dependencies, the one extracted with the fewest threads to cut—, and you saw why the extraction order is decided by the dependencies, not by business importance. And you put the net before touching anything: you recorded the legacy catalog's golden master —six cases with their quirks frozen (the volume-discount truncation, the inactive product charged the same)— and used it to catch a "clean" reimplementation that, with good intentions, rounded the bulk normally and stopped charging the inactives. The golden master caught the three regressions before they touched production, and you discovered that the quirk doesn't manifest in all cases (usb-hub matched), so characterizing with enough volume is what catches it safely.
Before moving on you should be able to: explain why the catalog is the convenient slice to do first; record a slice's golden master with its quirks included; use it in a parallel-run to catch a regression; and defend why characterizing preserves the behavior instead of improving it.
Lesson 3 takes the second step: putting the slice behind the facade. With the golden master already recorded as a net, you're going to interpose a strangler router in front of the catalog and divert the traffic by percentage from 0 to 100, with the old route as fallback. And you're going to read a revelation that connects directly with this lesson: at traffic_percent = 100, the fallback is still at 100 because the modern doesn't yet know how to calculate the volume discount —the same bulk whose golden master you just froze—. The traffic_percent measures what you tried; the fallback measures what the modern couldn't. This lesson's net will be exactly what, later, allows closing that gap.
Resources
- Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004) — the book that founds the method's first step: putting the code under characterization tests (freezing its current behavior) before changing it. The chapters on characterization tests and seams are the direct basis of this lesson. In English.
- Sam Newman, Monolith to Microservices (O'Reilly, 2019), ch. 3 — on choosing what to extract first by the dependency graph (start with what depends least on others), and the decomposition order of a monolith. The source of "the leaf first". In English.
- Martin Fowler, "Patterns of Legacy Displacement" — martinfowler.com/articles/patterns-legacy-displacement. Running the old implementation and the candidate over the same inputs and comparing their outputs; the parallel run as a named pattern is Sam Newman's (Monolith to Microservices). The exact mechanism by which the golden master catches this lesson's regression. In English.
- Emily Bache, "Approval Testing" and ApprovalTests — approvaltests.com. The golden master brought into practice: approving a reference output and comparing it on each run, reading the diff before re-approving. In English.