Capstone III: measure before and after
The refactor paid off only if the target changes are now cheap. Measure before vs after — edits per change, coupling, test seams, cohesion — against unit 00s cost-of-change lens, and know when good-enough beats endless gold-plating. The track closes here.
You spent two days untangling the order module. It looks better — smaller functions, real names, a tax boundary, a test or two. Your reviewer asks the only question that matters: “How do you know it’s better and not just different?” And then the harder one: “How do you know you’re done?”
“It feels cleaner” is not an answer a senior gives. Unit 00 gave you the lens that does answer it — the dominant cost of software is the cost of changing it — so the proof of a refactor is not how the code looks, it is what the next change now costs. This lesson closes the track by measuring that, and by naming the stopping rule that separates a finished refactor from a refactor that never ends.
After this lesson you can prove a refactor paid off the way a senior does: by taking the two target changes the capstone was scoped around — “add a new payment method” and “add a region-based tax” — and measuring them before and after as edits per change, coupling, test seams, and cohesion, all tied back to unit 00’s cost-of-change lens. You can also state an explicit stopping criterion so the refactor ends at “good enough for the changes we expect” instead of drifting into gold-plating.
Pick the metric that the whole track was about: edits per logical change. Aesthetics (“cleaner”, “nicer”) are unfalsifiable and not what the codebase pays for. The honest measure is the one unit 00 named — change amplification: how many sites you must find and edit to make one conceptual change, and how many of those are easy to miss. Score the two target changes both ways.
// BEFORE — "add a new payment method (Klarna)"
// touch PaymentForm.tsx (new branch in a switch)
// touch checkout.ts (new branch in a switch)
// touch refund.ts (new branch in a switch)
// touch reconcile.ts (new branch — easy to forget → silent bug)
// → 4 edits, 4 chances to miss one
// AFTER
// add KlarnaProvider implements PaymentProvider (1 new file)
// register it in the providers map (1 line)
// → 1 localized add + 1 registration, 0 existing switches touchedThe number that fell — from 4 scattered edits to one local addition — is the result. Everything else in this lesson explains why it fell.
Show coupling dropped, concretely — count the things that must change together. “Less coupled” is a claim; connascence and dependency direction are the evidence. Before, four modules were connascent of the payment-name strings: each switch had to know every payment type, so the four moved together. After, they depend on one PaymentProvider abstraction and know nothing about each other or about Klarna.
// BEFORE: every site is coupled to the full set of payment names
function fee(method: string) {
switch (method) { // checkout.ts knows: card, paypal, klarna…
case "card": return 0.029;
case "paypal": return 0.034;
// add klarna here AND in three other switches
}
}
// AFTER: sites depend on the interface; providers are independent
interface PaymentProvider { fee(): number; charge(amt: number): Promise<Receipt>; }
const providers: Record<string, PaymentProvider> = { card, paypal, klarna };The measurable drop: the number of files that must change together for “new payment method” went from 4 to 0 existing + 1 new. That is coupling, quantified — not a vibe.
Prove new test seams exist — by writing a test that was impossible before. A seam is a place you can substitute behaviour without editing the code under test. Before, fees lived inside a switch reached only by driving the whole checkout, so a fee test needed a fake cart, a fake user, and a network stub. After, KlarnaProvider.fee() is a pure unit you can assert directly — the seam is the interface.
// AFTER — a test that the BEFORE shape could not express in isolation
test("klarna fee is 1.9%", () => {
expect(new KlarnaProvider().fee()).toBe(0.019);
});
// region-based tax got a seam too — inject the rate table, no globals
test("tax for EU region", () => {
const tax = taxFor(100, "EU", { EU: 0.2, US: 0.0 });
expect(tax).toBe(20);
});The metric: testable units went from “fee logic reachable only through the full checkout flow” to “fee and tax assertable in two-line tests.” Unit 10’s safety net is what made the refactor safe; these seams are what the refactor produced.
Show cohesion rose — group the second target change and watch it land in one module. Cohesion is “things that change together live together.” The capstone’s second change — region-based tax — is the probe. Before, the 0.2 literal was smeared across pricing, invoicing, and reporting (low cohesion: one rule, many homes). After, the rule lives in tax.ts, so the region change is a single edit there and nothing else moves.
// AFTER — one home for the tax rule; region change is local to it
// tax.ts
export function taxFor(amount: number, region: Region, rates: RateTable): number {
return amount * rates[region]; // ← region logic changes ONLY here
}Measure it as before/after edits for “add region-based tax”: from 3 scattered sites (and a missed one becomes a bug) to 1. High cohesion is exactly what makes the expected change cheap — which is the only definition of “good” this track has used since unit 00.
Name the stopping rule, or the refactor never ends. The senior failure mode here is not under-refactoring — it is gold-plating: polishing past the point of payoff, adding flexibility for changes nobody asked for, chasing a perfect score on a module that is already good enough. A refactor is done when the expected changes are cheap and local — not when the code is flawless.
// STOP signal: re-score the two target changes.
// "new payment method" → 1 local add ✓ cheap
// "region-based tax" → 1 local edit ✓ cheap
// Both targets met → STOP. Do NOT now abstract currency, rounding,
// or a plugin system "while we're in here" — no change is asking for them.The stopping criterion is concrete: state the changes you expect, refactor until those are single local edits, and stop. “Good enough for the changes we expect” beats “perfect,” because perfect spends today’s budget on changes that may never come — the exact mistake unit 00 warned against when it said changeability is a bet, not an absolute.
Score the refactor on a table, not on vibes. Take the capstone module before and after, and fill in a before/after row for each target change. This is the artifact you put in the PR description — it is what makes “better” falsifiable.
// BEFORE — the messy module, scored against the two expected changes
// add new payment method: edits=4 coupledFiles=4 seams=0 missRisk=high
// add region-based tax: edits=3 coupledFiles=3 seams=0 missRisk=high
// AFTER — same two changes, re-scored
// add new payment method: edits=1 coupledFiles=0(+1 new) seams=1 missRisk=none
// add region-based tax: edits=1 coupledFiles=0 seams=1 missRisk=noneRead the table back through unit 00’s lens. Edits per change fell (change amplification gone). Coupled files fell to zero existing (coupling dropped — the modules no longer move together). Seams went 0 → 1 for each (the change can now be tested in isolation). Miss risk went high → none (no scattered site left to forget). Every column is a cost-of-change quantity from unit 00, and every one improved for the changes you were asked to make cheap.
Now the discipline that ends the track: look at the table and stop. Both target changes are single local edits. You will be tempted to keep going — a generic Money type, a currency abstraction, a config-driven tax engine. None of those changes is on the table. Adding them spends real reading-and-testing budget to buy flexibility for changes nobody requested. The refactor is done the moment the expected changes are cheap; “done” is a measured state, not a feeling that the code is finally perfect.
▸Why this works
Why measure changeability instead of the usual quality metrics — cyclomatic complexity, line count, coverage percentage? Because those are proxies, and proxies drift from the thing you actually care about. A module can have low complexity and high coverage and still be agony to change if its one rule is duplicated across six files; coverage says nothing about edits-per-change. The cost-of-change lens measures the cost directly: pick the changes you expect, count what each costs before and after. It is the only metric that can’t be gamed, because it is the goal. Complexity and coverage are useful supporting evidence — but the headline number is “what does the next expected change cost now?”
▸Common mistake
The classic capstone mistake is refactoring forever. The module passes both target changes cheaply, but it’s “not elegant yet” — so you keep extracting, generalising, adding seams for hypothetical futures. This is gold-plating, and it is genuinely expensive: every speculative abstraction is more code to read, another guess about the future that is probably wrong, and (per unit 12’s “wrong abstraction”) a structure that is harder to undo than the duplication it replaced. The fix is to commit, in advance, to the changes the refactor must make cheap — and stop the instant those are single local edits. Perfect is not the bar. Good enough for the changes we expect is the bar, and it is a measurable one.
Your capstone refactor makes both target changes ('new payment method', 'region-based tax') one local edit each, with new test seams. A teammate wants to keep going: extract a generic Money type and a config-driven tax engine 'while we're in here.' By this track's standard, what is the right call and why?
A refactor is proven the way unit 00 told you to measure everything: by the cost of the next change, not by how the code looks. You closed the capstone by taking its two target changes — “add a payment method” and “add a region-based tax” — and scoring them before and after as edits per change (change amplification fell), coupling (files that must move together dropped to zero existing), test seams (each change is now assertable in isolation), and cohesion (each rule lives in one home, so its change is local). Those four columns are the same cost-of-change quantities the track opened with, now improved on purpose. And the senior move that ends the track is the stopping rule: a refactor is done when the expected changes are cheap and local — chasing perfect past that point is gold-plating, spending real budget on changes nobody requested. Prove changeability, not aesthetics; stop at good enough. That is the whole track, measured.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.