Originally published at tddbuddy.com.
Related reading: Agents Should Do TDD argued for the loop. The Bar for TDD Just Moved named the floor. Agents Amplify Whatever Vocabulary They Find named the compounding. This post names the step where the compounding is authored.
Refactor is the step nobody counted.
For twenty years, "did you TDD" meant "did you write the test first, did you see it fail, did you make it pass." Three questions, and the third was almost always answered "yes, technically." Red and green produced visible deliverables. Refactor produced nothing shippable. It changed no behavior. It broke no bars. It shipped no feature. Under time pressure it was the first thing to go, and time pressure was the default. Most "TDD shops" ran red-green-and-move-on.
The codebases that compounded over a decade did the refactor pass. The codebases that accreted did not. From a single pull request, the two look identical. Five years in, one is the codebase teams point at as good engineering and the other is "the one we had to rewrite." The difference that held across languages, frameworks, and team sizes was thousands of small refactor passes, each worth almost nothing in isolation, compounding into the shape the codebase became.
Agents now generate red and green at speed, and they skip refactor by default, for structural reasons that mirror the human reasons and land harder. Without a discipline that makes refactor non-skippable, agent-driven codebases will accrete at generation speed.
Red and Green Were Always the Visible Two Thirds
The loop was described as three steps. It was rewarded as two.
Red produced a failing test that named an intended behavior. Green produced a passing test and the code that satisfied it. Both could be pointed at, counted, and reviewed. A commit that went from red to green shipped code, closed a ticket, moved a burndown. The metrics teams used to track progress rewarded the transition.
Refactor changed the shape of code that already passed its tests. From outside, the code did the same thing before and after. To a reviewer skimming the diff, the refactor commit looked like a cleanup. A courtesy. The author was thanked, the next ticket was picked up, and the pass was counted nowhere. Beck named refactor as the third step of the loop. The industry adopted red and green and treated refactor as optional polish; the training, the tooling, and the performance reviews followed.
The result was a two-step loop dressed up as a three-step loop. Teams called it TDD. What they ran was red-green.
The step with no green bar was the step that paid the rent.
Codebases That Compound Did the Refactor Pass
The evidence is not in any single commit. It is in the shape of codebases years after the first line was written.
A codebase that runs the refactor pass on every commit accumulates a specific set of properties. Naming aligns with the domain, because every commit is a chance to rename what turned out to be misnamed. Duplication stays small, because every commit is a chance to collapse the duplication that appeared. Abstractions are earned, because every commit is a chance to delete the speculative one that stopped paying its way. Each refactor is a tiny investment. Aggregated across ten thousand commits, they are the difference between a codebase whose next feature costs a week and one whose next feature costs three sprints.
A codebase that skips the pass accumulates the mirror properties. Names get set at first writing and are rarely revisited. The second implementation of a behavior looks slightly different from the first, and nobody consolidates them. The speculative abstraction from month two is still there at year five, doing nothing useful and shaping every addition. Each skip is a tiny debt. None of them, in isolation, is a problem. That is the trap.
The two trajectories look identical for months and diverge over years. The debt is invisible per PR and overwhelming per year, and no single point on the timeline offers much evidence of the divergence, which is why teams rarely notice it happening. The codebase that ends up costing three sprints per feature arrives there through ten thousand skipped passes, each costing "nothing" at the time.
The rent gets collected either way. The codebases that ran the refactor pass paid it a little at a time and compounded. The codebases that skipped it paid nothing for years and then paid a rewrite.
Agents Skip Refactor by Default
The reasons humans cheated on refactor apply to agents. New reasons apply harder. An agent will sometimes tidy what it touches; nothing in the loop makes that reliable.
Refactor has no green-bar reward. The agent's reward signal, in most environments, is coupled to green tests, passing CI, and merged PRs. The refactor pass moves none of them. Before the pass: green, passing, mergeable. After: green, passing, mergeable. Under pressure on context tokens or task time, the agent optimizes for the reward, and the reward is available either way.
Refactor has no prompt extension. "Add a discount for loyalty members on orders over fifty dollars" is a task the agent can complete by writing a test, writing implementation, and stopping. The prompt does not say "then look at the surrounding code and reshape whatever your change disturbed." A constraint the prompt does not state is a constraint the agent has no budget for.
Refactor requires context on the surrounding code. A meaningful refactor is not a local operation: it means reading around the change, spotting the parallel path just introduced, and consolidating. Under context pressure, the agent focuses on the local change, produces something plausible, and moves on.
Refactor has no crisp completion criterion. Red is done when the test fails. Green is done when the test passes. Refactor is done when the code is "clean," which is not a check the agent can run. Human practice relied on taste. The agent has less of it for the specific codebase, and uncertainty tips it toward "call it good and move on."
The result compounds. One agent generates a plausible implementation, the next generates another beside it, and nothing consolidates them. The shape is set by a decision nobody made.
Humans cheated on refactor under time pressure. Agents omit it under context pressure. The mechanism is different. The outcome is the same, at higher throughput.
Without Refactor, Speed Is Acceleration in the Wrong Direction
Consider three sprints of discount logic with no refactor discipline.
Sprint one: "add a loyalty discount for members on orders over fifty dollars." The agent writes a test and an implementation. The codebase gets ApplyLoyaltyDiscount(order) on OrderService. Green, merged.
Sprint two: "add a Black Friday promotion that stacks with the loyalty discount, but only for first-year members." The agent finds ApplyLoyaltyDiscount, considers extending it, and adds ApplyBlackFridayPromotion(order) next to it instead. The relationship between the two is encoded in the order they are called from OrderService.Process, and nowhere else. Green, merged.
Sprint three: "add a referral discount." Same pattern. ApplyReferralDiscount(order). The codebase now has three discount methods, three near-identical reductions, and three naming conventions: PercentOff, FlatReduction, DiscountAmount. No shared abstraction. Three parallel implementations of a behavior the domain has one word for.
Nothing in this trajectory looks wrong per PR. Each PR did what was asked, passed its tests, and shipped. The shape changed underneath, and none of the agents authored the change.
Now run the same sprints with refactor required on every change. Sprint one ships ApplyLoyaltyDiscount; the pass finds nothing to consolidate, because nothing exists yet. Sprint two's pass notices the shared structure and extracts a Discount abstraction that OrderService.Process composes. Sprint three's pass finds the abstraction waiting and adds ReferralDiscount as a third producer. Same feature set. Half the lines. One vocabulary. One place to look when the next discount concept arrives.
The refactor pass is what turns three sprints of shipped features into a codebase that can absorb the fourth without paying for the first three. Skip it long enough and the fourth sprint costs three.
Speed without refactor is acceleration in the wrong direction.
Refactor Decides the Codebase's Shape
The codebase's shape is not decided in the original write. It is decided in the rewrite. The first version of any piece of code is plausible. The second, informed by seeing the first in context, is closer. The third version is the design.
This is not a claim about talent. It is a claim about information. Design decisions need things the original author did not have: which abstractions the code shares with the rest of the system, which names read well next to their siblings, which vocabulary the domain has settled on. Most of that arrives only after the code exists and can be seen against what surrounds it. The original write produces something that works. The refactor pass makes it something that fits. Working code that does not fit carries a standing tax on change downstream.
Naming, in particular, is decided in refactor and almost nowhere else. The name a function gets at first writing is the name the author thought of in the moment, before seeing it alongside its siblings or noticing that the same concept has a different name three files over. Skip refactor and the wrong name propagates, because the next author, sampling the surrounding code, sees it and reproduces it.
Abstractions follow the same rule. The one reached for at first writing is right, wrong, or premature. Refactor confirms the first, replaces the second, deletes the third, and each of those decisions is only possible after the code exists. Skip the step and premature abstractions persist, wrong ones harden, and the codebase becomes a museum of decisions nobody would make now.
The same holds at the vocabulary layer. Consider a builder born in a hurried sprint:
// Before the refactor pass: every call site repeats the defaults
var order = createOrder(customerId, items, false, false, null);
// After: the builder reads like the domain
var order = anOrder().For(aLoyaltyMember()).Containing(items);
Run the pass and every future test that touches the builder inherits the better vocabulary; the migration is mechanical because the behavior is preserved. Skip it and three months later two hundred tests call the five-parameter version, agents generating tests copy the shape the surrounding tests show, and each new test lowers the ceiling on how much vocabulary work will ever be affordable. Same starting point, two very different codebases, and the difference was one refactor pass worth an hour that nobody would have counted as productive, because the tests were already green.
Vocabulary compounds in whichever direction refactor moves it.
The Discipline Move Is to Make Refactor Non-Skippable
Instructing the agent to refactor does not install the discipline. An instruction to "always refactor" competes for context tokens with the code and the tests, has no completion criterion, and has no green bar behind it. What works is structural: the workflow refuses to close the loop without the step.
Refactor as a required commit. Every feature PR contains at least two commits: the behavior change (failing test, passing implementation) and the refactor pass (whatever reshaping the code needed once the behavior was in place). CI rejects PRs whose history contains only the first. In an agent-driven workflow the same check becomes a pipeline stage: the PR is not ready for review until a distinct commit shows reshaping, and a PR without one is sent back. The refactor is now a visible deliverable, and skipping it is obvious.
Refactor as a separate review pass. The behavior review asks: does this code do what the test says? The refactor review asks: does the code around it now cohere? Giving the second question its own pass, ideally its own reviewer, keeps the behavior review's momentum from carrying past the design question.
Refactor as a metric the team tracks. Refactor commits per PR. PRs whose refactor commit was non-trivial. Time between a feature landing and the next refactor of the code it touched. None of these are perfect. All of them beat inferring codebase health from ticket counts. Refactor was invisible for twenty years because no metric touched it.
None of these are exotic. All of them are mechanical. The pattern is the same in each case: the codebase's shape gets its own commit, its own review, its own metric, because treating it as free is how it never happens. It is not free. It is the step that pays the rent.
The workflow that ships behavior without shipping shape ships debt.
The Compounding Was Always in the Third Step
Return to the opening claim. Refactor is the step nobody counted.
The two-step loop dressed up as a three-step loop was the industry's practice for twenty years, and the codebases that survived it did the third step anyway, because someone cared enough to do unrewarded work. Agents will not supply that someone. They run the loop at whatever level of discipline the workflow encodes. If the workflow rewards red-green, they ship red-green. If it requires the third step, they ship the third step. The discipline is not in the agent. The discipline is in the workflow.
The third step is where naming aligns with the domain, where duplication consolidates before it hardens, where the vocabulary tightens, where the shape gets authored. The industry described it as optional and rewarded it as invisible. It was always load-bearing.
Refactor is not a step of TDD. Refactor is the only step that makes the other two add up. That was true when humans ran the loop. It is more true now that agents do.
Skip refactor and the codebase is decided by the code that got typed. Run refactor and the codebase is decided by the team.
Top comments (0)