Joshua Jarvis

January 9, 2025

Purpose-Driven Refactoring

When it comes to refactoring tech debt, choosing the right time makes all the difference.

As software engineers most of us do a good job at identifying code we don't like. We negotiate that work into our roadmap or sprint. Unfortunately this has become a bit of a box checking operation. Worse yet, it can become reckless if the timing isn't right.  

Before I go further I would like to define what technical debt means to me. It's definitely one of those umbrella terms that holds a different definition for everyone.

Tech Debt is...

1. Deliberate design decisions made given a tradeoff, usually related to a deadline, that will one day be addressed. 
2. Accidental design decisions that aged poorly. This is usually caused by a lack of knowledge about the problem. 
3. Missing documentation or unit tests. 
4. Outdated practices, dependencies or frameworks that get in the way of shipping new features. 
5. Outright messy code or anti-patterns introduced by inexperienced developers. 

On paper these are all problems worth solving. Our natural tendency is to solve them sooner rather than later. But in reality these problems are only worth solving if they are felt and are dragging down the product. As bad as the code smells it can even be ok to just live with them. 

In any product with traction tech debt will be a constant. Even if everything was addressed there will be new instances as the product continues to evolve and change. 

Maybe this sounds futile but it shouldn't feel that way. Accepting it, identifying and recording it is the way forward. Now that we have identified it we need to decide when to fix it. 

This is where "purpose-driven refactoring" enters the picture. Using this model you only fix technical debt when there is a payoff that's greater than the risk. Some refactors are inherently risky but they can become less risky depending on when you choose to address them.

I'll give a recent example of technical debt I worked into a project as a "purpose-driven refactor" opportunity. Last year we created a new class that polymorphically manages e-signing lease documents for different providers. The team liked the new pattern and naturally felt that it would be useful to use that pattern in other places such as generating documents. 

That was a good idea but making that change in isolation would introduce change in one of the most critical parts of our Online Leasing product. This would come with no clear benefit for the end user and a significant risk of bugs. Instead we recorded that idea until the appropriate time came to revisit it. 

Just a few weeks ago that time arrived. We started planning for a new project where there are quite a few changes impacting the way we generate documents. We need to do this work and we will have the proverbial "hood of the car lifted" at this time. This is the perfect time to refactor document generation into a new class while we are in there. 

Using "purpose driven refactoring" de-escalates the risk of the tech debt by coupling it with the risk of introducing the new feature. In this case I believe that it even increases the likelihood of success for the project. At the end of the project the payoff is meaningful.

Finally, this is not always a hard rule. There are definitely examples of tech debt that are lower risk and have a high payoff. Sometimes you get lucky and it's one line of code that fixes a performance bottleneck or improves DevEx by an order of magnitude. But in general for any type of tech debt that doesn't have a visible payoff and introduces medium to large risk I prefer to refactor it with purpose.