Most software engineers with a couple years of experience know that Rewriting Is Bad. But we go ahead and do it anyway. Why?
In my opinion, we don’t rewrite code for practical reasons; we rewrite it because of the way the old code makes us feel. So perhaps we don’t need another airtight argument against rewrites (and it’s not what I’ll provide you here); perhaps we just need to rethink our relationship to our code.
People love to tell us not to rewrite stuff. Joel Spolsky wrote the canonical “things you should never do” post in April of 2000, and I read a new one at least once a year. Roughly, the logic goes:
- Rewrites always take longer than expected.
- Rewrites require us to maintain old and new systems in parallel for the duration of the rewrite.
- Legacy systems have fixes for hundreds or thousands of bugs that we don’t know about, and those fixes can easily get left behind during a rewrite.
- For those reasons, we should refactor, not rewrite. Instead of rewriting systems entirely, it’s best to make incremental improvements towards a better design.
It’s a tremendous argument, and it’s completely right. The practical move is basically always to refactor systems bit-by-bit instead of rewriting them.
So why do we still do rewrites all the time? I’ve been a part of several in the last year at my job, and I tend to hear about them constantly happening elsewhere. What’s up with that?
I think the reason is, at an emotional level we really really want to rewrite things. Rewrites give us a light at the end of the tunnel. We imagine a newly-rewritten Elixir codebase that’s perfectly factored and compiles in 0 seconds. We imagine hitting the delete key on 10,000 lines of code that was written by that Java guy who no longer works here. It fills us with joy and excitement. It gets us out of bed in the morning.
And then we take that excitement and argue that our situation is unique. We tell ourselves and our managers, “It’s an exceptional case: the code is terrible and riddled with bugs, and everyone hates working on it. Sarah even quit last month because this system is so poorly written. We’ll focus hard, do the rewrite, fix the bugs, and hopefully even have time to write an amazing blog post about our success.”
It turns out we’re actually pretty good at convincing ourselves and our peers that the system needs a rewrite. We’re not doing it because we’re incompetent. We’ve read Joel’s blog post. We know what we’re signing up for.
But we never seem to accept that the underlying reason is emotional: we’re rewriting because we’re angry, we’re frustrated, and we have a toxic relationship with the code. This anger makes us fantasize about greener pastures, where build times are low, everything is declarative, and things are “clean”. We see a rewrite as a way of vanquishing our enemy. A rewrite provides a cathartic escape from the shackles of our legacy system.
Sure, the practical solution is to refactor bit-by-bit. We know that of course. But the real problem is not the system itself, it’s our attitude about the system. The solution to that problem is not refactoring the system, it’s building empathy for it.
So my advice is: learn to love the legacy system. Find things about it that you appreciate. Immerse yourself in it, befriend it, learn its secrets.
It isn’t always easy to care about an old janky mess. Sometimes we inherit code from other teams, so we don’t have a shared history with it. Other times we are on a team that deals solely with a system’s problems and we never see what it’s good at. Often we aren’t given the time to improve it, and we take out our frustration on the original authors.
What can we do when we notice these negative emotions?
In my experience, there are a few ways to intentionally build empathy with a system:
- Use it. Use it in a way that works. Use it like a user would use it, or if you can’t, try to watch users using it. If that’s not an option, look at its production logs to see how people use it. The goal here is to understand what it’s actually good at. Old systems are generally better than we make them out to be—we spend so much time dealing with their problems and no time appreciating what they do well.
- Read it. Read the code, read old comments, try to dig up documentation that was written by its authors. Find evidence of a crazy bug fix that probably took days. Try to understand why the authors put a module boundary in a place that strikes you as wrong. Here, our goal is to make the system less intimidating, and also appreciate the complexity and where that complexity came from.
- Write about it. Surprisingly often, we don’t like old systems because they’re not documented or they’re poorly commented. Describing what you learned in steps 1 and 2 will not only help your teammates grow more familiar with the system, but also make you feel some ownership over it. Which brings me to:
- Improve it. I don’t mean “fix bugs” (often fixing bugs just makes us more frustrated) — find something that annoys you about it and make it better. Optimize your CI process so that builds finish a bit faster. Add some automated tests for the file you’re always touching. Pull a massive function into its own file and refactor it so it’s easier to read. Improving a system builds trust, respect, and ownership. After adding something that you’re proud of, you’re less likely to want to scrap the whole thing.
After repeating steps 1-4 several times, I often realize that my gut was wrong—there’s a lot of good in there, and the thought of re-doing all that work is more daunting than it is tempting. And even if my gut wasn’t wrong—the system truly sucks and does need a dreaded rewrite—then at least I know much better what that entails.
When we deeply care about the existing system, it’s easier to get excited about ways of making it better without rewriting completely. That’s the goal. That’s when “refactor, don’t rewrite” becomes fun and rewarding.