A few weeks ago I wrote that rewriting from scratch is almost never the answer. That leaves an honest question hanging in the air. If I should not burn the old code down, how do I add a new feature into a system I am genuinely afraid to touch? This post is about one answer I reach for often. Just one. Not the only one, and not the definitive one, but a small move that has saved me many times.
The idea is not mine. It comes from a book, and the person who put that book in my hands was Matt Evans, a senior staff engineer I worked with on a big legacy refactoring project. He taught me a lot, and this was one of those lessons that stuck.
What legacy code really means
Michael Feathers wrote Working Effectively with Legacy Code in 2004, and he gives the sharpest definition I know. Legacy code is simply code without tests. Not old code, not ugly code. Code you cannot change with confidence, because nothing tells you when you broke it.
That is the real fear. You open a method that is eight hundred lines long, the kind nobody wants to own, and your small new feature has to go in there somewhere. Every line you add to that method is a bet, and the house has no tests to catch you when you lose.
The sprout method, in plain words
Feathers describes a move he calls the sprout method, in chapter six. The idea is almost embarrassingly simple, which is exactly why it works.
Instead of writing your new logic inside the big scary method, you write it in a brand new method of its own. Then you call that new method from the old code, with a single line. The new logic sprouts on the side, clean and separate, instead of growing deeper into the mess.
// the old, scary method, almost untouched
public void processOrder(Order order) {
// ... 800 lines nobody wants to read ...
// the one new line: call the sprout
applyLoyaltyDiscount(order);
// ... more old lines ...
}
// the new logic, born clean and on its own
private void applyLoyaltyDiscount(Order order) {
// small, focused, and easy to test
}
When the new behavior is big enough, you do the same thing with a whole new class instead of a method. Same idea, bigger sprout. Feathers calls that the sprout class.
// the old method still barely changes
public void processOrder(Order order) {
// ... 800 lines nobody wants to read ...
// hand the new responsibility to a fresh, tested class
new LoyaltyDiscount().applyTo(order);
// ... more old lines ...
}
// a new class, born clean, fully under test
public class LoyaltyDiscount {
public void applyTo(Order order) {
// all the new rules live here, isolated and tested
}
}
A sprout class is the right call when the new logic has its own rules, its own data, or will grow over time. The old code only learns one new name, the class, and everything complex lives behind it.
There is also a nice bonus. Because the sprout is isolated, it is easy to wrap the call in a feature flag. You ship the new method or class turned off, enable it for a small slice of traffic, and turn it off in seconds if something looks wrong. The old code path is still right there, untouched, as your safety net.
Why this wins
The beauty is in what you did not do. You did not refactor eight hundred lines under deadline pressure. You did not bet the release on understanding code nobody remembers. You added one line to the old method and put all the real work somewhere safe.
A few concrete wins.
The new code is isolated. It lives in its own method or class, not buried in the middle of the chaos, so you can read it, name it well, and reason about it on its own.
The new code is testable from birth. Because it is small and separate, you can write tests for it right away. You are not adding new untested logic to an untested method, which is how legacy grows. You are adding tested logic next to it.
The old method barely changes. One line. The smaller the change to the scary part, the smaller the risk that you wake up an old bug nobody documented.
You leave the place a little better. Not by cleaning the whole room, but by making sure the new furniture you brought in is solid and covered, even if the old furniture stays untouched for now.
The business side, and the honest limit
This is the opposite of the big rewrite. You ship the feature today, you do not freeze the roadmap to clean an ocean of old code, and you still do not make the debt worse. The new value arrives now, and the new code is the good kind. That is a trade a manager can love and an engineer can respect.
I want to be clear though, the sprout method is one tool, not a philosophy. Sometimes the right move really is to refactor the old method first, especially if you will be working in it for months. Sometimes you need to wrap it, or carve a seam to get it under test before anything else. Feathers fills a whole book with these techniques, and the skill is choosing the right one for the situation in front of you. The sprout method is just the one with the best risk to reward ratio for the most common case, which is adding a little new behavior to a big old thing.
So when you face that eight hundred line method and your stomach drops, remember you do not have to go in and fight it. You can grow your feature on the side, test it, and connect it with one careful line. A small green sprout on an old trunk. That is how legacy systems get better, slowly, without anyone holding their breath.
Pax et bonum.