Breaking Dependencies in Legacy Code
Seams, characterization tests, and the Mikado Method for changing code you're afraid to touch
The most useful definition of legacy code I know is the one Michael Feathers offers: legacy code is code without tests. Not old code, not ugly code — untested code. The reason untested code is so hard to work with is simple. You cannot change it safely because you cannot tell what you broke. Every edit is a gamble, and the fear of that gamble is why these areas of a codebase ossify.
The way out is not to rewrite. It is to build the net first, then change.
Find the seams#
A seam is a place where you can alter the behavior of the code without editing the code in that spot. The whole challenge of getting hard-to-test code under test is that it tends to grab its collaborators directly — it news up a database connection inside a method, calls a static service, reaches out to the network. You cannot test that in isolation because you cannot substitute anything.
A seam is the lever. It might be a constructor parameter you can pass a different object through, a method you can override in a subclass, or an interface you can implement with a stand-in. Find the seam and you have found the place where a test can take control.
Get it under a harness, then characterize#
Once you have a seam, get the code under a test harness — enough scaffolding that you can call it from a test and observe what it does.
Then write characterization tests. These are not tests of what the code should do. They pin down what it currently does, including behavior that looks wrong. You run the code, see what it produces, and write a test asserting exactly that. If it returns a weird rounding or an off-by-one quirk, you encode the quirk. The point is not correctness — it is to capture the current behavior so you have a net before you touch anything. Once the net is in place, you can change the structure and let the tests tell you the instant behavior shifts. If some captured behavior turns out to be a real bug, you fix it deliberately, as its own change, with the test updated on purpose.
This is the same discipline that makes the difference in when and why to refactor — you do not refactor without tests, you build the tests that let you refactor.
State-based vs interaction-based verification#
The moment you introduce test doubles to fill those seams, you face a choice in how you verify. State-based verification checks the result: you exercise the code and assert on the value it produced or the state it left behind. Interaction-based verification checks the conversation: you assert that the code called its collaborator with particular arguments. Both are legitimate. State-based tests tend to be more robust to refactoring because they care about outcomes, not mechanics; interaction-based tests are sometimes the only way to verify behavior whose only effect is a call to something external. Choose deliberately rather than by habit.
The Mikado Method for structural change#
Characterization tests handle local change. For larger structural changes — the kind where touching one thing reveals five prerequisites — there is the Mikado Method, and it has saved me from week-long "everything is broken" stretches more than once.
The procedure:
- Attempt the change directly. Just try to make the change you want, naively.
- Record the prerequisites it surfaces. The compiler errors, the broken assumptions, the things that have to move first — write them down as nodes in a goal graph.
- Revert. Throw away the broken attempt entirely. This is the counterintuitive part, and it is the key. You do not push forward through the breakage.
- Do the prerequisites first, working bottom-up from the leaves of the graph. Each prerequisite, completed on its own, leaves the system working.
- Repeat until the original change becomes a small, safe edit.
The discipline of reverting is what keeps the system green the entire time. Instead of being broken for days while you wrestle a tangle, you make a sequence of small changes that each keep the build passing. The goal graph becomes a map of the work, and you can stop and pick it up later without holding the whole mess in your head.
Where this fits#
Seams and characterization tests are how you make a legacy area safe to touch. The Mikado Method is how you make a big change without breaking everything. And when the right move is to grow a replacement alongside the old code rather than transform it in place, that is the Strangler Fig play. Different tools, same underlying commitment: never change code you cannot verify.