In the past, I’ve written about technical debt, and how it’s an all-too-common problem with software development projects. In a nutshell, the basic concept of technical debt is this: You build technology, but as you cut corners or optimize for delivery speed, you incur future costs that you’ll have to pay off eventually—or your project will go bankrupt.
Get it now, pay later
Technical debt is a lot like taking out a 20-year bank loan to buy a house because you don’t have enough cash to cover it. You know you’ll end up paying more for the house in the long run, thanks to interest, but you’re okay with that. You’re willing to pay more to move into the house today.
So, how can you pay off technical debt and resolve these problems once and for all? One option is refactoring. Refactoring is the act of cleaning up your code, making it better and easier to read and maintain.
It's the technical equivalent of reorganizing your closet. You’re not necessarily changing the contents of the closet—you’re just streamlining and putting things in their proper place. This makes it easier for you to quickly find what you need when you need it.
But how exactly does refactoring work? What’s the best way to approach it?
“This needs to be totally rewritten.”
It seems as if every time a software developer takes over an existing project, they say something like, “This is the worst code I’ve ever seen. We should just delete everything and start from scratch.”
Maybe there aren’t any tests, the code style is out of whack, the libraries are outdated or it’s combination of all of the above.
Software developers like to play with the latest and greatest technology. In large part, that’s why they’re so passionate about their work. Because a software developer holds his craft in high regard, he or she doesn’t like to work on old, crappy tech.
But if you look at this from a business stakeholder perspective — chances are very slim that you’ll ever get the budget and time to do the BIG REWRITE every engineer is dying to do. There are also other risks to the BIG REWRITE. For one, you basically lock up all your resources for a certain amount of time. If X months is your estimate for finishing the rewrite, you effectively don’t produce any new value to your customers for X months.
Even worse, what usually happens is this: X months turns into 2X months. Joel Spolsky wrote about this ages ago —never rewrite your software.
So, what can you do?
Bring out your broom
There’s a popular 90s British sitcom called “Only Fools and Horses.” In the show, a character called Trigger (nicknamed after a horse, mind you), is a street sweeper. In one particular sketch, Trigger talks about how he’s had the same broom for 20 years.
When Rodney asks if he’s actually swept any roads with his 20-year-old broom, Trigger says, “Well, of course! But I look after it well. We have an old saying that’s been handed down by generations of road sweepers: ‘Look after your broom.’ And that’s what I’ve done; I’ve maintained it for 20 years.”
He then holds up his broom to show his friends and proudly proclaims, “This old broom has had 17 new heads and 14 new handles in its time!”
Baffled, the waiter asks, “How can it be the same bloody broom, then?” Trigger pulls out an old photograph of himself holding his beloved broom. He responds, “Well, here’s a picture of it. What more proof do you need?”
The Ship of Theseus
In ancient philosophy, this is also known as the Ship of Theseus (Theseus’s paradox) - if a ship (or any other object, for that matter) has all of its components replaced, is it still fundamentally the same ship?
So, what about a software development project? If you replace every single component of a software solution, is it still the same solution? Yes— but it will be a much improved solution, as long as you take the right approach.
So, what is the correct approach to refactoring large, legacy systems? It all comes down to replacing smaller systems inside larger, running, systems.
In other words, you should build out a new framework and new concepts inside the existing, already running, application. Construct any new features that you build with this new framework, and try to use the old, crappy parts of the system as little as possible. If you have no other choice, create bridges to the old code in a way that will be future proof.
Much like Trigger’s broom, you’re replacing the old, worn-out parts with more functional parts to ensure the solution is well-maintained. Remember those wise words passed down from generations of street sweepers: “Look after your broom!”
As part of this process, it’s important to continuously deploy this code, test it and have users use it in production. This is very different from the BIG REWRITE because we’re constantly battle testing our new code, hardening it and creating production-ready code.
So, we’re not just writing code with a million bugs that will be uncovered once we do the BIG LAUNCH after a year of work. We’re creating new code day after day, and deploying that to production regularly.
Shiny new code
If you use this refactoring approach, this is how things will unfold over time: Your codebase will start out with 100% old shitty code and 0% new shiny code.
As time goes by, you’ll implement new features, but using only new and shiny code. So, at this point, you have 90% old crappy code and 10% new shiny code.
As you keep doing this for all new features, you’ll eventually reach the tipping point where there’s more shiny code than crappy code. You can then pull off the BIG REWRITE, which at this point is basically eliminating the rest of the cruft.
Can it be done?
Of course it can! In fact, we pulled it off in 2016 for the core application powering Productive, a spinoff business born out of Infinum. We started off with a dated codebase that I personally wrote most of way back when.
The team took the Trigger’s broom approach, running both systems in parallel, and migrating parts of the architecture as they went along. In other words, they were slowly replacing those old, beaten-up broom parts with shiny new ones.
We strategized with users by redirecting them to the old parts of the system until they were rewritten completely. We ran two parallel JSON APIs until we didn’t need the old one anymore.
We chopped off small pieces of the old Productive here or there and extracted it to different microservices. We built the entire frontend on a completely new tech stack. Of course, the specs changed along the way, based on the business or user expectations, and that was fine. We weren’t stuck in rewrite hell, and we were shipping to production all the time.
Like Trigger’s beloved broom, we now have an application that’s basically the same from a business perspective — it’s still called Productive, and it still does the same thing (manages your agency workflow)— however it’s well-maintained and greatly enhanced.
Today, there’s not a single line of code in Productive from that old and dated codebase that I wrote. And believe me, this is a good thing— because I wrote pretty shitty code compared to today’s standards.
The next time you take over a dated or legacy codebase, and your inner instincts start saying, “Let’s just rewrite all of this,” try to change your mindset.
Ask yourself, “How can I add value, piece by piece, and replace certain parts of it, to make sure it will get better and better over time?”
It’s just like that old saying: “How do you eat an elephant? One bite at a time.”
(But please, don’t eat elephants. They’re extraordinary animals.)