A couple of days back I retweeted this tweet
If you and your team ship software then you always need to think about the tech debt that you’re accumulating as the product grows. Some of it is inevitable. After all
Real artists ship!
Nothing beats shipping your product and getting it out to your users. Often there will be a relatively small window of time in which you’ll need to get the product out. Getting your app or your product for the new version of iOS in September every year is an example. So you want to get new features, better performance, better stability and bug fixes into your product. As you race against time you will cut corners, you will skip writing tests and you will write hacks. As with most products you will be working with a team — ranging in size from a few developers and testers to several tens of developers and testers spread across different geographical regions and time zones. If your product has a beta program then at some point you’ll be mainstreaming whatever your early adopters have signed up for.
To sum up, as the product evolves, as new features get added, as your development team grows from just a single or couple of developers to more than ten, there will be the times when you will prioritize shipping your product over clean code, consistent design/architecture and writing tests. This is not only inevitable but just as it should be, probably.
All through this, though, you have to keep an eye out for the tech debt that gets built up and look to pay it back from time to time. Hence,
Refactor whenever and wherever possible.
This deserves to be deconstructed a little bit, however, because you’ll seldom be able to buy time in your sprints to purely refactor stuff. The Boss is gonna ask if you really need to do it, the PM wants the team to build out that cool new feature and of course users of your product are waiting for bugs to be fixed. So how do you really refactor, and really, is there any real value in doing it besides some ideal of “clean code” that’s always cherished but seldom realized in practice?
Why should you refactor your code, really? After all, hey, it’s clunky but it works! I’ll try and share what I’ve learned over the years about the pitfalls of not paying back your tech debt.
- Higher incremental cost of adding a feature. The cost of adding a feature is not just the cost of writing and testing it alone. Sure, early on in the life of your product, may be, yes. But as the product grows, as you add features, the cost of adding a _new feature_ is actually the cost of building it *plus* the cost of how it interacts with any _existing feature(s)_ in your product. If your code/design/architecture resembles a set of classes thrown into a soup of glue code, with hacks written around it, adding that new feature is going to be painful, as you discover that the new feature impacts what you’ve already built in ways that probably the PM or even the engineers never imagined. There are a couple of problems with this. One is a sort of a spiraling effect where complexity breeds complexity and you end up adding more hacks and band aid to get the new feature up and going and to fix any regressions in the existing features. The other, and perhaps more significant one, is that inevitably the original estimates that the engineer or your team has come up with are gonna all go awry, and at the worst moment possible — right at the time when you’re team has committed to give the first build to the QA team. And that’s just the best case scenario. Even worse is when bugs creep into the build and are discovered only when it lands into the hands of your test engineers. The cost of fixing these bugs is higher, too, because you’ll now be figuring out what was it that introduced that bug in the first place. If you’re working on a pretty substantial new feature, well, good luck with that! Now there’s confusion and commotion all around — your manager is wondering why is the build delayed, your QA team is up in arms about the build quality and the number of regressions that were introduced; besides of course, any bugs in the feature being written. You’re on the slippery slope to missing your timelines. Where did things go wrong, you ask? Well, may be in practice it won’t be as bad as the scenario painted above, but you already know the answer. You blame the developers before you that left the code in the shape in which you found it, and gripe a little and move on. If you had, however, spent some time refactoring then perhaps that cost of interacting features may not have been so bad, or at the least, you’d have been better positioned to have a more realistic estimate of the complexity of building this new feature!
- A Ground-Up Rewrite. The part above on shipping a new feature happens a few times over — obviously, if your product is reasonably widely used, you’re going to be adding new features at a fair clip — and what you’re left with is a state at which it is virtually impossible to add a new feature without the change footprint spreading to most of your code. When that happens someone says, “Guys, we should just rewrite the whole thing from the ground up!” Justifications will be given, lofty ideals will be stated and everyone gets to the task with gusto. But wait. Every week, every month — and if your product has a fair bit of legacy such an effort will take months — spent into rewriting the product from the ground up is time taken away from adding new features to it! Your competition meanwhile is gonna be at it — at improving their product, at listening to its users, to give them what they really want. Your competition is either gonna be catching up with you or moving leaps and bounds ahead of you! Mind you, sometimes a rewrite is essential. But you want to do it for the right reasons — because you want to leverage a new technology or to leverage a new version of the OS better, or to make your offering *significantly* lighter, say. You don’t wanna do it because there’s no other way forward when adding a new capability to your product.
- Maintainability. You think you’ve heard this one before. Well, think again. Of course readability and maintainability is important in itself. But it is imperative that your code is maintainable to encourage your team to add features that *they* thought of, as well, besides the ones that are handed out by the PM. It’s imperative that your code isn’t a big ball of mess if your team has to be able to roll out that fix pronto with a high confidence of not rocking the regular sprint tasks. So you want to refactor your code to enable your team to work on a maintainable repository not something that they throw up their hands at.
When to Refactor
Well, so it’s important to refactor your code — you always knew that, and if you read this far you hopefully see value in investing time to do it. Great! But wait, the PM or the Engineering Manager is probably not gonna buy you and your team time just to refactor some component(s) of your code. Sure, you can load your sprints so that you can carve out some time to refactor parts of your code. But how do you figure out when you need to buy some time — explicitly or implicitly — to refactor your code. There’s probably no one-size-fits-all answer but here are some indicators.
- When you’re merging a significant patch or change request into your mainline/trunk. Not all features are equal. Some are small, contained; others impact the codebase significantly. When you view the pull-request to merge this change you’ll usually see the footprint of the feature you’re building. It helps to have a “template” of questions to ask yourself or your team when reviewing such a patch. Does it change your core models or a number of models? Does it change your component interfaces? Does it change a core functionality that is used across multiple features? Usually if the answer to one or more of these questions is yes, then you should look at that patch more closely and see if there is merit in refactoring some parts of your code.
- When you’re mainstreaming a functionality that has so far being in beta. This is a good time to check if your design/architecture needs a change for the better. Often a beta track may be run by a small set of developers working separately from the main product. And you inherently want to iterate faster on the beta track — pushing out updates faster than your usual cadence so that you get that invaluable feedback from your early adopters. When merging your beta functionality into the mainstream you want to ensure that the conceptual complexity of your architecture/design is not significantly increased. This then increases the incremental cost of adding a feature as described above. This may need a fair bit of refactoring to your component interfaces — after all, your beta may evolve in ways unexpected and unforeseen when the original architecture was conceived.
- When there is a lull in PM asks. This one is, again, obvious, but requires a significant bit of discipline and it is, as I call it, about showing your product some Tender Loving Care. Most products will have some inherent seasonality and peaks and troughs associated with the product demands — like let’s say the post-holiday quarter is often much lighter than the one that precedes it. During such times, get your team together and hammer out a list of refactoring tasks that are must-do/good-to-do.
What to Refacor
So here we are. You’ve figured out the needs and benefits of refactoring your code. We have a sense of what are good opportunities to carry out your refactoring efforts. Now when you actually have the resources — in terms of time and engineering manpower — you have to figure out which parts to refactor. While this might seem fairly obvious, it is about extracting the maximum bang for the buck. Remember, even during the lean times, you could spend the days and weeks implementing a new, cool feature that one of the engineers came up with in her spare time! Also remember that refactoring is not an end in itself but rather a means to an end. You want to refactor so that
- You can keep the conceptual complexity of your software reasonably low.
- Adding new features does not skew our development costs unfairly due to multiple interacting features.
- Detecting and fixing bugs is easier and can happen without any side effects.
- You can enable your team to add new features on their own — or, at least, not let the code base be a disincentive to implementing that cool feature that an engineer came up with.
With this in mind, here are some guidelines that can help you get the maximum returns — against the above objectives — for your investments in refactoring your source.
- Refactor parts that change frequently. Not all parts of the source, not all components are created equal. Some see a lot of change, others see only very rare changes. You can look at a repository’s history to see what are the parts that have changed often. If something has changed often, then it’s likely to see continued evolution in the future, too. And this makes it a good candidate to check if it needs to be refactored.
- Refactor parts that have empirically been found to be source of bugs. Look at past JIRA tickets or issues on your project. What were the components that had the incidence of most bugs or bugs that were the hardest to find and fix or bugs that had the biggest impact? Often such bugs creep in because of higher-than-is-ideal complexity — this makes it difficult for developers to correctly understand what that component/module/class/method does and is the reason for bugs to be introduced. Look at these carefully to decide whether refactoring these parts can simplify the interfaces/design and reduce the complexity.
- Always be refactoring your core engine. There will always be a part of your code that is central to your product. This may be your business models or your rendering components or a service that talks to other components. Much like your car, it’s important to keep your engine well oiled and free of dirt. The core being riddled with big-balls-of-mud is always a red alert and points to a need to refactor that part.
Alright, then. We’ve been over why to refactor, when to refactor and what to refactor. What is expressed here are general indicators rather than specific rules or instructions. Tailor them to suit your needs, the realities of your project, and Always Be Refactoring!