top | item 37966485

Refactoring has a price, not refactoring has a cost

185 points| todsacerdoti | 2 years ago |germanvelasco.com | reply

178 comments

order
[+] bloopernova|2 years ago|reply
We're dealing with accumulated tech debt burying us right now.

For years the team was told to deliver features, that the upgrades to node dependencies weren't of a high enough priority. Now some of the dependencies are so old they're complicating upgrading past node 12.

Yet the cybersecurity people are demanding that vulnerabilities be fixed.

Thankfully we have a stronger product owner now, and spent most of the last 6 months getting things updated. Yet there are still those in senior leadership who openly question why our team hasn't delivered the features they want.

We're working on educating them. And something pithy and catchy is useful, so I'm definitely going to mention this in future: "fixing tech debt has a price, not fixing it has a cost."

It's the kind of thing that if you can get a "cool" or influential senior leader to repeat, the rest will fall in line.

[+] rr808|2 years ago|reply
Node 12? Holy s** that is only 4 years old. Meanwhile in the real world most applications are running on 10+ year old stacks - Java 8, Python 2, Angular.js etc etc
[+] kstrauser|2 years ago|reply
> Yet the cybersecurity people are demanding that vulnerabilities be fixed.

Talk to them. They may have your back. It was always a happy day when the security audit we were going through at that moment wanted us to attest that all our dependencies were sufficiently updated. That meant we had an external mandate to do the work we'd been wanting to do for ages anyway.

When I was a CISO, I would've been glad to "order" one of my colleagues to do that if they asked me to.

[+] tomrod|2 years ago|reply
> Yet there are still those in senior leadership who openly question why our team hasn't delivered the features they want.

Is this _really_ so hard for stakeholders to understand? I've found some limited success by dual-boating -- upgrading as features are created. "We can't build this feature without an upgrade" carries a lot of weight -- but sometimes you just have to bite the bullet and demand an upgrade.

Are stakeholders completely oblivious to the existential threat to their bonus if their company is hacked and driven to bankruptcy in lawsuits? Sure some bigs can avoid this, but not the mids and smalls.

[+] earnesti|2 years ago|reply
I was working for a company, where the engineering team was doing this kind of refactoring, test development and what not mainteinance coding with large team for about 2 years. Meanwhile not many new business features were being developed and the competition took over, and eventually everyone got fired. I think the leadership was just too lazy to actually push the development of the product, but let the engineers do this pussy work of endless refactoring was big part of the problem. Not only one of course.
[+] preommr|2 years ago|reply
> It's the kind of thing that if you can get a "cool" or influential senior leader to repeat, the rest will fall in line.

sympathetic-lol at the desperate copium.

[+] godelski|2 years ago|reply
> why our team hasn't delivered the features they want.

Good work takes time.

But I think it is an optimization problem. Rather, that different people are optimizing different things. Your team is optimizing long term profits. The senior leadership who's frustrated is optimizing short term profits. I think one way to handle these situations is to clarify which is the more important optimization problem. That makes points like "opportunity costs vs marginal cost" discussions. The same way we turn down immediate profits to go to school, because in the long run we make more money (and for other reasons). But our systems often push us to focus on short term goals because these are far easier to measure.

[+] n0us|2 years ago|reply
It’s okay we can’t get past Node 8
[+] kibibu|2 years ago|reply
The "debt" metaphor is a good one already though, and one that senior leaders should well understand.
[+] gardenhedge|2 years ago|reply
A bottom up approach to tech this way obviously does not work long term. Startups will eat your lunch.
[+] pacificmaelstrm|2 years ago|reply
I've been at a company where we had two leading developers.

One refactored everything endlessly to a fault, wrote tests for everything and emulators for every external piece of hardware.

The other rarely/never refactored and was focused on ends-justify-means functionality.

They hated each other and progress was very slow because they could not work together at all.

My two cents is that all products of engineering are temporary. Software (and hardware) are not eternal. What you write today you will eventually become obsolete.

Your entire app will eventually be re-written. If not by you then by your competitor who takes you out.

There is no such thing as perfect or optimal code because what is great for now won't be great for tomorrow.

With that in mind, good refactoring is like good math. Taking a long and complex system of code and replacing it with something functionally better and far simpler and easy to understand.

Bad refactoring is just moving things around for "understandability" or in a lot of cases one developer rewriting something in their own idiosyncratic style because they don't understand the style of the programmer before them.

And the final point is don't be afraid to rewrite the entire system if you have that ability.

Since the development process is about defining functionality as much as enacting it, once you have a finished application, you can re-create it much faster and simpler than it was originally written.

The Apollo missions got to the moon and back with 145000 lines of code you don't need 20 million + for your web app.

[+] RangerScience|2 years ago|reply
Been having this fight a lot lately. The phrase that seems to have struck a chord is ye olde "first you make the change the easy, then you make the change".

As a further elaboration, I'm trying out that this gets you:

- Better position. Code's better, so product is better (more stable, with better features, etc) - Higher velocity. Each time you do this, the overall codebase gets easier to change, because you're continually making it easier to change - High acceleration, because you're giving your devs a way to stretch their capabilities and grow their skills.

Definitely gonna see how "Refactoring has a price. Not refactoring has a cost. Either way, you pay" plays with folks!

PS -

AFAICT, future proofing is bad when you build stuff to aim it, mostly because you probably won't end up with the problem you think you will, so you're not actually able to build it correctly now.

But!

You can build in gaps, for where that new stuff can go. You can future proof pretty much only by writing your code today with eye towards making unknown changes later - ye olde "the only architecture that matters is the one that lets you change".

[+] karmakaze|2 years ago|reply
Yes! this quote captures my reasoning of factoring before rather than after. The other problem with factoring after is that it's doing so without knowing the future new motivations. I would say that the factoring after is to suit making a working prototype first, then when a factor is identified aligning the design/source along those lines.

Note I say factor rather than refactor since in most cases the existing code wasn't factored in the first place. It's also a reminder that we should know what that factor is before making changes. In some cases there is a new reason that changes which to factor but is less common IMO.

[+] nuancebydefault|2 years ago|reply
I often see that code is being refactored and there you have it, refactored code. But usually the problem is not in the code but rather in the design / architecture that has driven the code.

The design was made without a diligent machine like a computer with compiler checking it. Design reviews (by people) are boring and it hurts when someone pokes holes in one's design that has come to them after a few days and or nights of thinking. Also it is often based on unclear requirements.

So that's how code is born, which is based on suboptimal or even backward designs.

As soon as there is need for refactoring, usually only the code itself (a whole of tiny design decisions) is revamped, keeping the initial design alive.

[+] jacquesm|2 years ago|reply
Refactoring has a fixed cost, not refactoring has an ever increasing cost. Clearly either way you will pay but if you refactor you will end up paying much less in total. The bigger problem is not the cost per-se but the resource allocation issue associated with refactoring: you can go bankrupt with very clean refactored code that gives your competition time enough to bury you with a crappy codebase and the features that the customer needs.

All of these are balancing acts. It's never 'refactor or not', it's how much to refactor and what to let go? These are strategic issues and they go hand-in-hand with business goals and knowledge about the longer term roadmap. For instance it would be pointless to refactor a codebase that will end up being supplanted by another product or for which a strategic acquisition is planned of a company with a technically superior product.

[+] sokoloff|2 years ago|reply
> Refactoring has a fixed cost, not refactoring has an ever increasing cost.

That means that refactoring has an increasing cost as well (with increased frequency, the total money spent on refactoring increases).

[+] lionkor|2 years ago|reply
It's also sometimes "when to refactor", because eventually you either have to refactor or rewrite (or retire).
[+] blackoil|2 years ago|reply
Tech debt is like all other debts, it need to be serviced. You keep accumulating it and it becomes toxic. Never take any debt and you are leaving easy money on table and may loose to competition because you have slower delivery.
[+] reactordev|2 years ago|reply
I agree with this. I’d go further and say it’s a visibility issue. Non-tech folks don’t know that the stuff is out of date, vulnerable, no longer supported. They don’t know how much debt they have borrowed. So having leadership know the costs, the cogs, the debts, and the projections is vital.
[+] somewhereoutth|2 years ago|reply
The most foolproof way to avoid regressions is to leave the code alone.

The best testing is battle testing - if it is working well after plenty of contact with the customer then leave the code alone.

Bitter experience has taught me this.

---

I'm going to expand on my comment a bit - we need to be clearer on what (production) code actually is. We believe it is the definition of the runtime behavior of a given system (and of course it is exactly that).

However, for any reasonably complex system, a complete understanding of the definition/behavior is beyond any single person. Thus code is in fact a recording of the social and technical process of producing the system, and as such embeds decisions and solutions that may not be evident to anyone just reading the code.

When viewed in this light, it is easy to see how dangerous refactoring can be.

[+] iopq|2 years ago|reply
That just leaves the code in a state where it's not clear what's wrong with it, because it works most of the time. There are just two types of code: obviously correct, or not obviously wrong.

If you write some code and you don't know how it works, it's most likely not correct in some edge cases.

Code that is written to be so simple it's obviously doing the right thing is better than battle tested mess of if statements.

I've given interviews of fizzbuzz and even though developers usually recognize they need a separate case for 15, they don't always do the else ifs right (maybe test for the 15 case last when you've already hit the divisible by 3 case first, or similar silly mistakes). In that case, doing the string accumulation refactor makes it clear that the code has no mistakes because it for sure adds a Fizz first and a Buzz second in the 15 case. It's also a good refactor for extending to Bazz (divisible by 7) as well, so it's something you will want to do when you want to add that feature

[+] dataflow|2 years ago|reply
> The most foolproof way to avoid regressions is to leave the code alone.

The most foolproof way to avoid car accidents is to avoid ever being around a car.

Surely that's not the only objective, right?

[+] seadan83|2 years ago|reply
Testing is a form of battle testing. Code that has been thoroughly tested is akin to a running system for a year. On the other hand, modify that legacy system without test, that "battle test counter" resets to zero and you get to go through the process again.

Classically speaking, refactoring is only for code that has thorough coverage. Refactoring code that is not under test might be better worded as rewriting.

Obviously, rewriting critical and complex code that has evolved over time with a variety of fixes and random features bolted on with random conditionals - obviously that is risky

[+] gt565k|2 years ago|reply
Sure, but a lot of poorly written spaghetti code that works is sometimes impossible to extend or add new features to without a major refactor or complete rewrite.

Currently dealing with some 13 year old objective C code for a mobile app that’s gonna require a major rewrite cause it looks like it was written by someone who should have never been writing code in the first place

[+] marcosdumay|2 years ago|reply
You can only ever leave your code alone if it's modular and independent enough so that new changes on your system do not depend on changing it.
[+] RangerScience|2 years ago|reply
I mean, yes and no, and aside from that - the article isn't even about regressions.

> embeds decisions and solutions that may not be evident to anyone just reading the code.

Strongly agree! Good code communicates this to other programmers (including yourself in 3-6 months). If the code doesn't do a good job of this, that alone can not only be worth a refactor, and when you refactor while "what it took to understand what this does" is fresh in your mind, you can write the new version that much more effective at communicating those learnings.

[+] joshspankit|2 years ago|reply
It’s fantasy, but if each battle was distilled in to a test and added to the test suite then it would be possible to touch production code without breaking business functionality.
[+] mumblemumble|2 years ago|reply
I don't think it's as simple as that.

I agree that one should not take something like this lightly. Far, far too many "refactorings" really just boil down to hasty rage-coding at some predecessor who had differing stylistic opinions, or keeners who've never heard of Chesterton's Fence getting frustrated with some annoying design element they don't understand.

But also, sometimes the code really is tangled and difficult to maintain, such that it's difficult to make any modifications without introducing regression defects.

Concrete example: I recently did a refactoring of a piece of code my team maintained that was so extensive that, by the final iteration, it was arguably a rewrite. The old code was sprawling and complex, could basically only be comprehended by the staff engineers, and tended to be ground zero for defects. The new code is much smaller and simpler, and easier to modify, and every single member of the team is comfortable reading it.

- but -

I only did this after months of careful observation, quietly building a case for the change and a plan for how to do it. This process included interviewing everyone I thought might know anything about this module, including folks who had moved on to different teams, to make sure I understood as much as possible of what was there and why it was built that way. I discussed my plans with them and sought (and, ultimately always got) their blessing. And then I spent more time bolstering the test coverage, including hunting for edge and corner cases that the existing test suite had missed, when necessary, adding additional tests that did not depend on this module's implementation details so that they would not be broken by the refactor. And then, when I finally did do the actual refactor, I didn't do it all at once. I broke the work into many small, carefully controlled iterations.

[+] AnimalMuppet|2 years ago|reply
True of fixing bugs, too. The odds of introducing a bug when fixing a bug is between 20 and 50%. (Per one study from decades ago. If you can produce better numbers, please do.)

But the problem is, even if code is battle-tested, someone always wants it to do something more. To get it to do more means changing it. And when you've changed it, they'll want another change. And then another.

So you either don't make changes (even bug fixes!) or you do. If you do, you either refactor the code as you change it, or you don't. If you don't, the code becomes more and more brittle, that is, harder and harder to change without breaking something.

So I think you are drawing wrong conclusions from true observations. You are correct that you can break code when you change it, even by refactoring it. Keep it in a state where the probability of that is minimized - where it's clear what the code is doing, and how, and why. To do that, you need to keep refactoring.

[+] zeroonetwothree|2 years ago|reply
Refactoring for no reason is not useful. But what happens in practice is you have a new requirement and you realize your hacked together “battle tested” system can’t possibly support it without breaking everything. So then you need to refactor first.

And if you are actively developing then refactoring along the way is generally better than if you wait until the very end.

[+] rizky05|2 years ago|reply
it works when the requirements never change. If somebody demands change, you screwed.
[+] jillesvangurp|2 years ago|reply
The cost and price scale non linearly to the amount of change. So, waiting to refactor has an exponentially higher cost the longer you wait whereas doing lots of small refactoring as you go has a relatively low cost. To the point where it's just negligent not to do it. You can spend five or ten minutes cleaning up a little or you skip it. Not doing it is sloppy. Just apply the boy scout rule.

People have the wrong reflexes where being overly conservative and fearful to change a working system eventually leads to a system that becomes so hard to fix that people stop bothering to even try and just replace or retire it in the end. Which is very expensive and destructive of course. Usually such decisions become a reality when people leave or change teams and it turns out there's nobody around that even has a clue where to begin fixing the old thing. Getting rid of the thing then becomes the logical choice.

Calling it technical debt is a way of talking yourself into believing you can deal with it later. Quite often that doesn't work out that way. Another fallacy is believing the debt is something with a fixed/known price that you can pay off at the time of your choosing. You can't; it's literally risk that accumulates that you haven't properly analyzed and estimated yet. Some of it might be fixable, some of it might not be. You won't know for sure until you sit down and do the work. Deferring the process of finding out is what gets teams into trouble. The less you know what you can still fix the more likely it is that the cost has become stupendously high.

Half the work is just knowing what needs doing and how it needs to be done. That doesn't mean you do the work right away. But spending a lot of time chin stroking over stuff you could do without actually doing stuff (aka. analysis paralysis) is also not productive. Finding a balance between knowing what needs doing and then just balancing new stuff vs. making sure that the cost of that new stuff isn't overly burdened by broken old stuff is the name of the game. Having a lot of technical debt usually translates into work that ought to be quick, simple, and easy being anything but those things. So, the technical debt metaphor means that you burden the cost of future essential work with that extra burden. Until you fix it, it only gets worse and worse. You pay the interest on every change until you fix the problem. And you won't know how expensive that is until you do it.

That's why you should refactor a little bit all the time. Low cost, it lowers your risk, and it keeps you aware of all the pain points and weak points in your system.

[+] nunez|2 years ago|reply
Agreed. Refactoring is like cleaning your house. Spending ten minutes cleaning/organizing your place daily is much less painful than waiting until that one day a month and spending 3-4 hours doing it. At least for me, anyway.
[+] ttoinou|2 years ago|reply
Either way you pay but the goal is to generate more revenue from your software than what you pay

  But one day all the cruft calcifies, and the numerous incompatible changes and features grind our progress to a halt. It is suddenly impossible to add new features without breaking something else
That's also the case if you keep refactoring every day, there's no reason to believe the changes you're doing right now are that good.

Refactoring and paying constant attention supposed code perfection has a huge unknown price that you have to pay very soon, whereas not refactoring has a future cost. You should use the money saved not refactoring right now to make something better later once the tech debt is too high but you know you have a good working software.

In any case, this kind of article needs tangible examples to illustrate...

[+] candiddevmike|2 years ago|reply
Any sort of refactor needs to have agreed upon goals/scope and folks need to stay within that scope. This is a really difficult skill IMO. Even when I start out with the best intentions of refactoring libraryA, suddenly I'm in a bad place refactoring libraryD because libraryA was refactored to use a new enum approach and damnit I want it to be consistent everywhere.
[+] jandrese|2 years ago|reply
Attempting to future proof your code so you never have to refactor: the most expensive choice of them all.
[+] blowski|2 years ago|reply
Yes, for most business applications, in the long run it’s cheapest to optimise for refactoring than believing you can avoid it.
[+] corethree|2 years ago|reply
There's no theory on good design. You organize your code one way versus another way nobody can really point to any mathematical proof or empirical evidence for anything. It's just one baseless arrangement vs. another.

This is why, often, when you refactor code it doesn't make it better. You don't fully know if it's better. It's just another way. So even when you do this refactor stuff often you find yourself in the SAME situation even if you didn't touch it.

Think about it. You follow best practices, you try to write code as beautifully as possible. That doesn't actually prevent a new feature from having you to gut a huge portion of your code to accommodate it.

But developers don't realize this. They think the extra time spent refactoring puts them in a better place then before. But really they just can't see the counter factual. Did it really?

I've been in places that emphasize on good code quality and put lots of good practices in place but they end up with "tech debt" anyway. Then when clean up day comes they blame it on bad practices and shortcuts but they're blind to the fact that the they were one of the most rigorous companies in terms of code quality.

[+] sz4kerto|2 years ago|reply
Refactoring is generally not about the code style, or changing the design for its own sake.

Refactoring happens because the model of the world that the software works with has changed since the software has been implemented. Maybe the way users and roles are represented was kinda OK 5 years ago, but the world (== business environment, understanding, requirements) has changed since. So the code that has been OK 5 years ago is painfully mismatched with the present.

That's when refactoring is needed.

Also. Tech debt is usually (not always) the marginal cost of the next feature. If new features cost X, and engineers are convinced that features could cost much less if the software was organised differently, then it might be useful to calculate the cost of a refactor.

[+] lionkor|2 years ago|reply
One way I've seen this enforced is this: Have your "best" devs, the people who put out 2x the amount of solid, testable, well documented code as anyone else, you know those that carry the sprint, spend half their time reviewing. Your overall sprint velocity may go down a bit, but your code quality will go up.

There's a magic to slowing things down, too - require reviews on every change, require tests to run and pass, run UI tests on each PR. Let people discuss good and bad code they see. Turn every little thing people mention, (like "man, this view is difficult to reason about", "the entire xyz class is a mess", "does anyone know why we do X here?") into a Refactor issue, discuss it, plan it, do it. You can do this during a big feature sprint, you dont need to wait until your product is done (then its too late).

Refactor, let people say "yes this issue isnt high prio, but its important to me". Embrace when people want to make code better.

[+] karmakaze|2 years ago|reply
I often find myself (re)factoring when beginning work if the codebase is not in the shape (design rather than quality) that it needs to be in for the particular new features to fit well within it. The reason I wouldn't factor after implementation is that it should have followed a design that already identified the factors that are being separated.

Whenever there's continuous refactoring, the factors aren't so much known and decided at a high level, it's typically more an exercise of deduplication. If you can't name the thing (or why) you're factoring it's blind deduplication, which still has some value but is much lower. If someone says they want to refactor, I'll ask what's the factor and why. A system separated along well chosen seams allows things to naturally and easily fit in its design.

[+] nathan_compton|2 years ago|reply
Does this essay actually say anything non-obvious? Like everyone knows there are relative costs. That isn't the hard problem. The hard problem is evaluating those costs correctly.
[+] guillemsola|2 years ago|reply
I like this sentence as it's something business stakeholders can unddo. And even the article focuses mostly in refactoring it can be used with many other good habits.

For instance monitoring has a price to set it, configure it... You can avoid part of the price by setting up a simple monitoring It has a cost as you're not going to catch early on issues in prod. And the latter you realize something is no going well, the higher the cost will be to fix it, loss clients...

And we can go on with many things as well, UX has a price, testing has a price, backups have a price...

Hopefully the writer don't think he has found the final reason to justify why refactoring is a must as there are many other best practices were low price will imply higher cost at the end. Wasn't that what we used to call tech debt? XD

[+] SonicSoul|2 years ago|reply
oh wow.. refactoring is an art form. just because your code "works" doesn't mean you have to leave it alone. your working code will likely be copied into 5 other projects and will not scale well.

ideally you're investing into creating shareable code libraries and into infrastructure that makes the process of maintaining/referencing shared components easy enough for engineers to want to dedicate their precious sprint time to it.

[+] myspeed|2 years ago|reply
Refactoring is OCD, you can do it infinite times. Many times a working code is rearranged to match some one's preceptive of business logic.
[+] majkinetor|2 years ago|reply
Either way, somebody pays, not me necessarily. Clearly different thing then claimed. Current project owner pays for sure. But who will that be when collection happens, and what team will be directly involved, that's entirely another thing. My bad habits may cost me nothing, there is also probability of gain.
[+] AnimalMuppet|2 years ago|reply
If I"m the person who winds up inheriting the code you produce, I'm probably going to resent you.

Do what you can, as if you were going to be the next person. For one reason, because anything else is irresponsible laziness. For another, you may in fact be the next person - you may not leave in time for someone else to inherit the mess. So do what you can now to minimize it.

[+] j45|2 years ago|reply
One aspect of technical debt is simply accumulating less until the lightbulb moment arrives to include mandatory refactoring. Build the least number of features you need to.