Modern XP: The Code Quality Loop
Tests make refactoring safe. Refactoring keeps the design simple. Simple design makes tests easy to write. Break the loop and the whole thing unravels.
This is the first of four posts on XP’s reinforcing practice loops. If you haven’t read the series introduction, start there for context on why these practices work as a system, not as individual techniques.
TDD, refactoring, and simple design are the foundation of XP. They are also the three practices most commonly attempted in isolation and most commonly done wrong. A team that writes tests after the code is not doing TDD. A team that cleans up code once a quarter is not refactoring. A team that debates design patterns in code review is not pursuing simple design.
These three practices form a loop. Each one depends on the other two. Adopt all three and you get a codebase that stays clean, testable, and easy to change over time. Adopt one or two and you get frustration, rigidity, or both.
Test-Driven Development: what it actually means
TDD is three steps repeated in short cycles. Write a failing test. Make it pass. Refactor. Red, green, refactor. The cycle takes minutes, not hours.
Most teams that claim to do TDD actually write tests after the code. This is a different activity with different outcomes. Writing tests after the code means you’re verifying what you built. Writing tests before the code means you’re specifying what you need. The difference matters.
When you write the test first, you design the interface before the implementation. You think about what the caller needs, not what the implementation does. This produces code that is easier to use, easier to test, and easier to change. The test is a specification, not a verification.
When you write tests after, you’re fitting tests around existing code. The code wasn’t designed to be testable, so the tests are harder to write. They often test implementation details because that’s what’s visible. They’re brittle. They break when you refactor even though behavior hasn’t changed. Developers learn to resent the test suite because it punishes change instead of enabling it.
The cadence matters
TDD is not “write all the tests, then write all the code.” It’s a conversation between the test and the implementation, happening in cycles of a few minutes. Write one test. Make it pass. Clean up. Write the next test.
This cadence forces small steps. You can’t get lost in a rabbit hole when the next thing you need to do is make a single test pass. You can’t over-engineer when you’re only writing enough code to satisfy the current test. The discipline of small steps produces code that is simpler than what most developers would write if given free rein.
Refactoring: continuous design, not periodic cleanup
Refactoring is changing the structure of code without changing its behavior. It’s not rewriting. It’s not fixing bugs. It’s not adding features. It’s improving the design of existing code in small, safe steps.
In XP, refactoring is not a scheduled activity. It’s not “refactoring sprint” or “tech debt week.” It’s the third step of every TDD cycle. Red, green, refactor. Every few minutes, you look at the code you just wrote and ask: is this the simplest structure that supports this behavior? If not, you improve it. Right now. Not later.
This is where the dependency on tests becomes clear. Refactoring without tests is risky. You’re changing structure and hoping you didn’t break behavior. So teams without tests don’t refactor. The design freezes in whatever shape it took when it was first written. Over months, that frozen design accumulates complexity. Concepts that should be separate are tangled together. Duplication creeps in. Names drift from their meaning.
Teams with tests but without the habit of continuous refactoring have a different problem. The tests make refactoring safe, but nobody does it. The code works, the tests pass, so why touch it? Because the design is slowly degrading. Every feature added to an un-refactored codebase makes the next feature harder. The cost is invisible on any single commit and obvious over any six-month period.
What refactoring looks like in practice
Refactoring is small moves. Extract a method. Rename a variable. Move a function to the class where it belongs. Inline a needless abstraction. Replace a conditional with polymorphism.
Each move takes seconds or minutes. Each move is backed by tests that confirm behavior is preserved. The cumulative effect is a codebase that stays clean without heroic cleanup efforts. The design evolves alongside the features, not in spite of them.
Simple design: four rules, zero ambiguity
Kent Beck defined simple design with four rules, in priority order:
- The code passes all tests.
- The code reveals its intent.
- The code contains no duplication.
- The code has the fewest elements possible.
That’s it. No mention of design patterns, SOLID principles, or architectural layers. Those are tools you might use in service of these rules, but they’re not goals in themselves.
Rule 4 is the one teams violate most. “Fewest elements” means no extra classes, no extra methods, no extra abstractions beyond what the current requirements demand. It means you don’t build the framework, the plugin system, or the configuration layer until you need them. It means three similar lines of code are better than a premature abstraction.
Simple design is not under-engineering. It’s the discipline of building what you need now, with clear intent, without duplication, and nothing more. When requirements change (and they will), you refactor to accommodate them. You don’t predict them.
This is where the dependency on refactoring becomes clear. Simple design only works if you can change the design later. If refactoring is expensive or risky, you have to predict the future and build for it now. That leads to over-engineering: abstractions nobody uses, extension points nobody extends, configuration options nobody configures.
With continuous refactoring backed by tests, you can afford to build the simplest thing today. When tomorrow’s requirements arrive, you reshape the code to fit. The cost of change is low because the tests make it safe and the habit makes it routine.
The loop in action
Watch how the three practices reinforce each other in a single development session.
You start with a test. The test describes what the code should do, not how it does it. You write the simplest implementation that makes the test pass. This is simple design in action: no extra structure, no speculation.
The test passes. You look at the code. Maybe the method is doing two things. You extract a method. The tests still pass. Maybe a name doesn’t communicate well. You rename it. The tests still pass. This is refactoring: safe because the tests are there, habitual because it’s part of every cycle.
You write the next test. The new requirement doesn’t fit the current structure cleanly. Instead of jamming it in, you refactor the structure to accommodate both the old and new behavior. The tests confirm you haven’t broken anything. Then you make the new test pass with a simple implementation.
After an hour, you have code that handles several scenarios, has clear names, has no duplication, has no speculative structure, and is covered by tests that describe what the code does. The design emerged from the tests and the refactoring, not from an upfront design session.
This is what most teams miss. The design doesn’t happen before coding or after coding. It happens during coding, continuously, driven by the feedback loop of red-green-refactor.
What breaks when you remove a side
TDD without refactoring: The test suite grows, but the code structure never improves. Tests start testing implementation details because the implementation wasn’t designed, it was accumulated. Over time, the tests become a liability. They break on every change, not because behavior changed, but because the brittle structure shifted. Developers start deleting tests or skipping them. The safety net disappears.
Refactoring without TDD: You improve structure but have no reliable way to verify you haven’t broken behavior. So you refactor cautiously, in large batches, with manual testing. The feedback loop is slow. Risk accumulates between refactoring sessions. Teams get burned by a refactoring-induced bug and stop refactoring entirely.
Simple design without refactoring and TDD: You try to keep things simple, but without tests you can’t safely change the design, and without refactoring you can’t evolve it. So “simple design” becomes “the first design,” frozen in place. New features get bolted on. The original simplicity erodes into accidental complexity.
The modern toolkit
The principles haven’t changed since 1999, but the tools are better.
Test runners are faster. Tests that took seconds now take milliseconds. The red-green-refactor cycle can be tighter than ever.
IDE refactoring tools handle extract method, rename, move, and inline with a keystroke. Refactoring that required careful manual editing in 1999 is now automated and safe.
AI coding assistants can generate test scaffolding, suggest refactoring moves, and identify duplication. They’re most effective when working with well-tested, well-structured code. TDD and simple design make AI assistance better, not redundant.
Type systems in modern languages catch entire categories of errors at compile time. This doesn’t replace tests, but it means your tests can focus on behavior rather than type correctness.
The barrier to entry for this loop is lower than it’s ever been. The excuses are fewer.
Getting started
If your team doesn’t practice all three today, don’t try to adopt all three at once. But understand the goal: all three, working together.
Start with TDD on new code. Don’t retrofit tests onto existing code. When you write a new feature or fix a bug, write the test first. Get comfortable with the red-green-refactor cadence.
Make refactoring part of every pull request. Not a separate task. Not a separate ticket. When you touch code, leave it better than you found it. Small moves, backed by tests.
Question every abstraction. When someone proposes an interface, a factory, or a base class, ask: do we need this today? If the answer is “we might need it later,” that’s a no. Build it when you need it. Refactor it in when the time comes.
The code quality loop is the foundation of XP. The next post covers the knowledge sharing loop: pair programming and collective ownership. These practices determine whether the code quality loop scales beyond individual developers to entire teams.
Found this useful?
If this post helped you, consider buying me a coffee.
Comments