Skip to content(if available)orjump to list(if available)

Reasons Not to Refactor

Reasons Not to Refactor

31 comments

·February 4, 2025

poulsbohemian

I spent about a 12 year period of my career doing triage and app performance work. What struck me was how often the core problems were architectural. Either the design was bad from the beginning, or the requirements had changed so dramatically over time (think scaling, for example) that the architecture no longer worked. I bring this up because while I saw a lot of questionable code, at the micro level it really didn't matter. Sure it could have been refactored and improved, but that would have been essentially diminishing returns. Often the improvements that were needed would have been so painful that rather than make them, I watched companies spiral around either throwing hardware at problems or replacing the application entirely because that kind of Bandaid rip was deemed "easier" in the overall politics of the corporate world. So point being - sure, refactor what you can, but don't get too hung up on things that ultimately won't matter.

lelanthran

Simple and brief rules are more successful in practice than long and complicated rules.

I feel a briefer and more-to-the-point "When To Refactor" guide is to ask the following questions in the following order and only proceed when you can answer YES to every single question.

1. Do we have test coverage of the use-cases that are affected?

2. Are any non-trivial logic and business changes on the horizon for the code in question?

3. Has the code in question been undergoing multiple modifications in the last two/three/four weeks/months/years?

Honestly, if you answer NO to any of the questions above, you're in for a world of hurt and expense if you then proceed to refactor.

That last one might seem a bit of a reach, but the reality is that if there is some code in production that has been working unchanged for the last two years, you're wasting your time refactoring it.

More importantly, no changes over the last few years means that absolutely no one in the company has in-depth and current knowledge of how that code works, so a refactor is pointless because no one knows what the specific problems actually are.

recroad

I'd extend #2 to any change, not just non-trivial. It's the classic Kent Beck tweet, "for any change, make the change easy (warning: this may be hard), then make the easy change"

Fixing my mental model of thinking about refactoring as a separate thing from "normal" development was key for me. Once I viewed refactoring as a thing that you do all the time as part of development, then I stopped even asking this question.

null

[deleted]

PaulHoule

I'd argue with that. Small-scale "micro-refactoring" operations are safe almost by definition. [1]

It depends on your language. In a large Python system [2], all bets are off, but in Java if you use your IDE to rename a method to have a clearer name or change the signature of a method or extract or inline a method the risk of breaking anything is close to zero whether or not you have tests.

Personally I think there is no conflict between feature work and micro-refactorings. If micro-refactorings are helpful for a feature you're working on, jump to it!

Personally I've had the experience of being a Cassandra [3] when it comes to YAGNI; [4] maybe it is different for a junior dev but so often I go to a meeting and say "what if you decide you need to collect N phone numbers instead of 2?" and over the next few months there is a stream of tickets for 3, 4, 5 phone numbers until they finally make it N. Or a problem w/ the login process that is obvious to me becomes the subject of a panic two months later.

As such I see the rule of three [5] to apply DRY is too conservative, it is way two common that the senior dev wrote two cases and then the junior dev comes in and copies it 15 times. At least on the projects I've worked on (mainly web-oriented, but many involving 'intelligent systems', data science, etc.) people have made way too many excuses for why repeating themselves is good and it has had awful consequences.

[1] https://en.wikipedia.org/wiki/Code_refactoring

[2] the best case for Python is that a large system in some other language could be a small system in Python

[3] https://en.wikipedia.org/wiki/Cassandra

[4] https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it

[5] https://en.wikipedia.org/wiki/Rule_of_three_(computer_progra...

scarface_74

> Do we have test coverage of the use-cases that are affected?

With statically typed languages and good tooling like JetBrains ReSharper, there are guaranteed safe automated large refactorings that can be done as long as someone isn’t using reflection.m

poulsbohemian

>the reality is that if there is some code in production that has been working unchanged for the last two years, you're wasting your time refactoring it.

So much this. I recall watching people early in their careers who wanted to make their mark go after code like this that was just a waste of time and more likely than not to blow up things downstream they didn't understand. And sadly watched managers praise their tenacity rather than understanding the explosions that were being created.

eyelidlessness

One reason I might accept one or more NOs to your questions:

Does the refactor support pending work, which isn’t directly related to the refactored code, but benefits from the lessons learned and applied in the refactor… even in some indirect way?

This might be providing a clearer pattern you’ll apply to similar new functionality; or it might be providing a new abstraction or even eliminating a failed abstraction which sets that pending work on the right path.

AnimalMuppet

Yeah. I did a (very small) refactor. It took, IIRC, four days. When I was done, I could write the new thing I was implementing in 10 new lines that used the newly-refactored existing code.

andrei_says_

These are very simple and clear and confirm my own approach to refactoring. Thank you.

ClumsyPilot

> More importantly, no changes over the last few years means that absolutely no one in the company has in-depth and current knowledge of how that code works, so a refactor is pointless because no one knows what the specific problems actually are. reply

That’s is the actual reason you might need to rewrite it. If your critical system is written in an ancient script that nobody can understand, and does no longer supported, and as a security risk, at some point it will simply stop working. And there will be nobody that can fix it.

Yes, rewrite may be painful, but if you can no longer find the people to support the old thing it may be necessary

gspencley

> We often reach a point during refactoring where it seems like there is an easy improvement that applies to almost all cases. It’s usually better not to impose additional abstraction if it only matches “almost” all cases.

Need to nit-pick on this one. There are design patterns, depending on your programming paradigm, that enable you to de-dupe the common stuff while allowing for all of those little variances to do their own unique thing without duplicating anything.

In OOP the Template Method pattern comes to mind.

I would even go so far as to say that the "almost all cases" problem is such a common problem in software development, that the patterns world has come up with various solutions to that which are intended to simplify, not complicate.

I'm sure there are concrete examples where there are no good pre-existing solutions, and there are often examples where cures can be worse than the diseases and you need to make a judgement call. But don't give yourself an excuse to duplicate code because you don't know how to de-dupe in the "almost all cases" scenario. That's a very solved problem.

hylaride

Refactoring depends heavily on circumstances. I've seen terrible attempts over my career for all the reasons other people have and will mention here.

But I've also seen huge successes. Usually it boils down to "what are you trying to accomplish?" against current code and architecture. I've seen targeted refactors of micro-service code, including language shifts (eg python -> java where threading improved things a lot in that use case), as well as targeted shifts of code to AWS Lambdas, where execution scaling also benefited the use case. But in both examples there was a clear benefit, execution plan, and way to measure success.

When you start refactoring large mono-app code bases for nebulous, hard to measure reasons then the risks are much higher (and that's even before scope creep comes into play). This gets even worse if the reason the current code is no longer desirable has reasons (both good and bad) that it is the way it is, and you risk recreating bad code (this was mentioned in the article) into new bad code. Also, migrations to new code can suck if the underlying dependencies, including data(bases) is just as much part of the problem?

BeetleB

Everyone talks about refactoring code. What I don't see enough discussion on is refactoring tests.

It's a constant battle at work, and I tend to be firm with: "If the test is working, no matter how horrible the (test) code, leave it be."[1]

For regular code, we rely on tests to validate our refactors. With test code, we don't have that support.

If you have a bunch of tests that could do with a refactor, and your motivation is that you need to write more tests, then set up whatever abstractions you need and write the new tests, but don't touch the existing tests! If you really want, you can overtest: Write new tests with your refactored code that test the same thing as the old ones. It's OK to test the same thing multiple times (when testing is cheap).

About a third of the time a coworker looks at my tests and refactors them, he makes an error - the most common one being not testing what the original test meant to test. See, the reason he wanted to refactor was that the original test was hard to follow. And because of that, he failed to interpret it correctly and failed to properly reproduce the test in his refactored code.

I then have to code review all his refactors. I have to spend time to figure out what my difficult-to-understand test did, and confirm he didn't introduce bugs. It's very, very tiring. And for what?

And he's not a newbie. He's as good at SW as I am. Plenty of experience. If he gets it wrong, I expect most people will get it wrong. With a higher error rate.

This is the one case where I say: "Feel free to overcomment." If you took a long time understanding a given test, write out what you learned so that the next time you read it, you know what it does. I'll be happy to code review that.

[1] Unless you are the original author of the tests, and they are still fresh in your mind. In that case, refactor all you want.

foo_barrio

This is a good point! We've gotten some instances of Chesterson's Fence that some devs casually remove during a "test refactor" that later allowed a regression to make it into prod.

I've caught some errors in "test refactoring" from our multiple levels of testing having large overlap with teach other. Our end-to-end tests have a lot of overlap with the integration tests which in turn have a large overlap with unit testing. The unit tests run in a matter of seconds compared to the end-to-end which can take minutes or in some cases hours for our manual testing so the levels of testing also serve as an efficiency for us.

kylereeve

When refactoring tests, some form of mutation coverage [0] would be really nice. Verify that the tests break when the underlying code changes.

[0] https://en.wikipedia.org/wiki/Mutation_testing

bob1029

If it won't impact your margins or the customer's willingness to pay for the product then it is difficult to justify.

I've been involved with (and responsible for) many "developer aesthetic" refactors over the years. They feel good in the moment but after blowing 2-3 weeks with regression testing, hot patches, etc., you start to wonder if it was all worth it.

There is stuff that is properly nasty and needs to be dealt with, but if you are spinning your wheels on things like namespaces being stuck on old company/product names, I would just give it up and move on. The average customer cannot see any of the things that frustrate us unless they are being done very poorly.

magicalhippo

We've got a lot of 20+ year old code in production, a lot written late at night in a crunch. Much of it's ugly, non-optimal and screams to be ripped out and replaced.

But it is working, in areas that see little change and are not overly performance sensitive.

So we only do something with it if we need larger changes. Otherwise we leave it be and spend our time being productive elsewhere.

tikhonj

> There’s no benefit to improving code that never changes, even if it is highly complex.

Only if you don't care about being able to understand and debug the code in the future!

And even then it's if you're confident in predicting how much code will or won't need to change, which, well...

mason55

The point is not to improve it until you actually need to change it.

Like you said, you're probably bad at predicting which code is going to need changes in the future, so if it's working now and you don't need to change it then it's a silly risk to try to improve it right now.

taeric

I like these reasons.

I will offer some caution on rules for rejecting a proposal. Often times, the number one reason to do something is that there is energy there to do it. If you can take that energy and apply it somewhere else, by all means do so. All too often, suppressing some energy stops all effort.

merb

When working with git it’s better to split refactorings, bug fixes and features per commit or merge request (if you squash) else your git history will be not so useful anymore.

PaulHoule

The default-oriented answer to #2 is to pass the rows through some function that applies the default to 'loud' if loud is undefined. That's really pretty code and avoids the complex if-statement which is a universal bad smell.

karmakaze

I have one reason to refactor that's served me well. Does the code exhibit ongoing edge/corner case bugs, which are attributed to the implementation not being factored in the first place? Another sign of this situation is exhaustive test cases which combine factors as they can't reliably be tested in isolation. Distributed logic is another common pattern, where a new feature is made by sprinkling bits of logic here and there, just so, and it all seems to do what's required. You know the kind, where onboarding a new person might lead to the discussion of how this section of your Rube Goldberg contraption works. A fancy way to this world is a poorly designed and maintained home-grown DSL.

Continuing to build on such a 'foundation' is madness that spirals to code red situations: stop-the-world, no feature development, until shit gets stable.

GiorgioG

Does anyone really get time to refactor anything these days? Seems like (at least where I'm at), everything is go-go-go, feature work.

foo_barrio

I try to never ever use the word "refactor" or "clean up" in any of my work items or even commit messages. My boss and other teams I am collaborating with are like clients to me. Restaurant bills typically do not include explicit line items for "dish washing, mopping, grease-trap cleaning, oil disposal, etc."

The downside is that other people/teams can appear to work much faster and put you at a disadvantage or even risk of being let go due to this perception. Unfortunately this is where IMO you have to play the game and make sure you toot your own horn to the right people sometimes.