Why You’re Not Being Allowed to Refactor
I’ve been thinking a lot lately about why it’s so hard to refactor legacy code. Not even just the physical act of refactoring, but finding time in our schedules, often artificially chopped into two-week segments, to do the work. I’ve complained about how it can seem really difficult to get approved to address the technical debt that always feels like it’s piled high when I join a new project.
So I thought I’d look at why that is and what we as developers can do to make it better. Let’s start by looking at the engineering manager’s perspective.
Resistance From the Business
A lot of times it feels like managers prioritize feature work over reducing technical debt. And there’s a lot of truth to that perception. Obviously, finished features have the potential to improve revenue, whereas refactoring offers the possibility of a future state where features are faster and easier to add.
The possibility. But the truth is that not every refactor achieves this. I’ve seen more than one refactor that not only did not make it faster to make changes, it broke a lot of things that were working that then had to be fixed. When managers are weighing whether to devote resources to refactoring work, they are not just considering the opportunity cost of not doing more feature work, they are skeptical if your refactor will even deliver the promised benefits in maintainability and extensibility.
One thing they know for sure is that the refactor will almost certainly break things that were working before and consume QA resources. The fewer tests you have the more true this is. And, let’s face it, if a codebase has a lot of friction in adding new features, it’s unlikely it has many tests.
Let’s Be Honest
So how do we overcome this? The first thing is to honestly understand where your current skills are for refactoring. It’s easier to build code that’s easy to build and maintain from the beginning than it is to refactor.
This is because when you refactor, you need to
- Look at a bunch of spaghetti
- Divine its intent
- Figure out how to fulfill the intent in a simple way
- Do that within the confines of a codebase that’s not designed for you to be able to swap out pieces
- Break as little as possible, including hidden dependencies.
You can clearly see that the list of skills you need to have for refactoring is long. It’s a huge learning task to take on, and you’re more likely to be successful if you learn in steps. The items on the list that are most approachable within the context of doing the approved feature work are
- Be able to solve problems in a simple way
- Be able to create logic where different pieces can easily be swapped for other pieces as requirements change
If you don’t have those skills, there’s no point in asking for time to refactor, because your new solution is unlikely to improve enough on the original solution to make it worth the hassle. So start there.
Upskill Yourself
How do you develop these skills?
On The Job
Well, it’s theoretically possible you could get there simply by prioritizing simplicity and maintainability in your new code. I haven’t ever seen this happen, but that could be because most of the developers I’ve worked with had other priorities rather than because it couldn’t have happened if they had set maintainability as their priority.
The thing I have seen work is for developers to develop test first. This is because writing the test first makes you really consider the fundamentals of the solution before you even start. Then, writing the simplest code that could possibly work starts you on the path to simpler code. The fact that you need to write a test for each new thing the code should do will keep you on it.
You’ll also find yourself breaking things up into smaller pieces, because it’s easier to test small, simple pieces. And the very fact you can bootstrap each piece in a test will make it much easier to plug and play the different pieces.
The only problem with this plan is managers often look at the time spent writing tests as wasted time. And there’s no denying it does take time to have enough understanding of testing strategies that you’re as fast or faster than just throwing stuff at the wall and hoping it will stick.
So, yes, the time between when you start coding and when you move it into the next bucket in your ticketing system may be longer. And unfortunately most teams look at that number to decide who’s fast and who’s slow versus considering how many extra times the code has to go through QA or how easy it will be to maintain the code.
However, you can potentially get away with this strategy if there’s testing infrastructure already in place, even if it’s not used very much, and there are process issues that make it so that you can’t really make progress in the real running project fairly regularly. For example, a couple of years ago I worked on a team where you couldn’t work on the live code if the build was broken. It frequently was because the Typescript rules were relatively strict and Typescript skills on the team were, to put it mildly, uneven.
So not only was I able to set up React Testing Library in a project that had like 3 tests, all of them around the Redux infrastructure, I was able to work test-first and be more productive than the rest of the team, which frequently just couldn’t work. Part of the reason I could perform well even after taking on the task of setting up significant test infrastructure is that it wasn’t my first rodeo with testing.
I wouldn’t attempt this on company time unless you have buyin from the business and/or you just have no other tasks you can work on. This can still be risky, though, because I’ve worked on teams where being seen to work on something unauthorized can get you in trouble, but doing nothing is fine. I have no idea why this is, but I’ve seen it over and over.
On Your Own
The safest way to learn these skills in on your own time. It’s not fair that businesses actually don’t give you the time to build code in the way that will best build their businesses, but it’s kind of understandable because return on that investment can be unpredictable. So we as developers sometimes need to take it on ourselves to put ourselves in a position to do that.
It’s really common for people to create side projects to learn new technologies, but there’s nothing that says the new technology is all you can be learning when you create that project.
I stand by my contention that TDD is the best way to learn to build well-architected code, but I’ve said that to enough people that ignored me that I know the percentage of readers who will devote time to that in their side projects is likely small. Consider prioritizing having modular code that can easily be swapped out in your side project over finishing it.
I never finished the project where I experimented with Next 13, but I learned a hell of a lot about other ideas in the process, including Puppeteer and using PostGres in a networked Docker container to facilitate testing.
You can also volunteer your time to an Open Source project, but you will need to work within their constraints and you may have a hard time finding tasks that let you work on the skills you’re trying to focus on. One area most Open Source projects need help with is documentation. If a piece of functionality isn’t documented, often the best way to figure out what it does is to read the code, including any tests the project may have, to see what it does. This is an excellent way to learn how to deconstruct legacy code.
Once You’ve Upskilled
You’ve put in the time where you know how to write code that’s better than what it’s replacing. Now what?
First, look for a task for new work in an area of the code where you feel you have a good grasp of what the code does that is a pain to work with for some reason. It might be that there are three separate copies of the logic that need to be updated. It might be that there’s just a lot of extraneous logic that you have to sift through to find the actual change.
Go through it and see if you can think of at least two ways that you could write code that’s better than what’s there. Try to see all (or at least most) of the business cases covered by the existing logic and think of how your solutions would address them. Estimate for yourself how long you think each solution would take. Now double it.
Estimate for yourself how long you think it will take to do the new work without refactoring. If code needs to be refactored, that’s often because there’s something about it that makes changes to that area of the code take longer than they should. So you may be surprised to find that your doubled estimate isn’t that far off what changing the code would take without doing the refactor.
When you find one where your best, most honest doubled estimate is no more than about one-and-a-half times what the time to make the change without refactoring, that’s when you should consider making a case for a refactor. Point out all the pain points around working with the existing code and outline your solution and tell them how much you estimate the refactor will be vs. just working around the pain points, and let the business make the decision. In the end, they’re on the hook for the consequences of that decision.
Eventually, you will be allowed to refactor something. When I do a refactor, I have the following goals:
- Drastic reduction in the amount of code to maintain
- Increase in clarity
- Accurately hit my estimate, even if I have to put in extra hours
- No more bugs than if there hadn’t been a refactor.
That last one can be hard to achieve, because often there are hidden business rules, but it’s very important. This is because bugs that surface in areas of the code that have been stable for a long time stick out like a sore thumb, whereas bugs that are introduced through normal feature work or other bug fixes are seen as a natural and expected part of the development process.
This is another place TDD can help you — if you’ve documented the requirements of the old system in tests, then your confidence you haven’t introduced bugs goes way up.
How This Works in Practice
I’d like to tell you if you just follow the above steps, you can refactor to your heart’s content and everything will be fine, but I can’t. My experience is that businesses are easily frightened with refactors.
Even if the refactor otherwise goes well, they will scrutinize the results more closely than work that is done without a refactor. Bugs that occur in the part of the system you refactored are because of the refactor. Bugs that occur in parts of the system that need to be refactored are not because they need to be refactored (however, one strategy to be able to get to do a refactor is correctly predict upcoming bugs over a period of time).
You really need to hit it out of the park in a refactor to not leave the business gun shy about refactors. But I think it’s worth doing, because when a refactor is done well, it dramatically improves the developer experience and does, in fact, reduce the amount of time spent working in the code that was cleaned up.
If you enjoyed this post, you might also like:
PlainEnglish.io 🚀
Thank you for being a part of the In Plain English community! Before you go:
- Be sure to clap and follow the writer️
- Learn how you can also write for In Plain English️
- Follow us: X | LinkedIn | YouTube | Discord | Newsletter
- Visit our other platforms: Stackademic | CoFeed | Venture





