Not all Technical Debt is the Same

In finance, your loan typically comes with an interest rate. Technical debt is arguably analogous to a loan with non-zero interest. Interest rates come in different packages. Some are higher than others, and this is the case for technical debt as well. Let me illustrate this.

Here’s an example of a simple and naive method to query for an IP address on a Linux machine.

Step 1: Query IP address

This is a simple program that queries for an IP address on Linux. Notice how we’re running a subprocess UNIX command and parsing the logic manually based on a returned string.

Assuming that this ip addr query works as expected, this should return something like this:

➜  ~ python temp.py
162.242.245.193

Here are a few points I want you to think about.

  1. You have a strict deadline and a bunch of other features that needs to be built on top of this get_ip_address() method. In other words, they call get_ip_address() and do something with the returned IP address.
  2. Those features depends on get_ip_address() , but does not need to know or utilize states/variables within the get_ip_address() interface.

Do any of these 2 require an immediate refactor on get_ip_address() , assuming the application itself is limited to Linux support?

No. There is “less ideal” but correct code. This technical debt is essentially an overhead. The debt itself is evident but do not slow down development as long as it maintains correctness as agreed upon in the definition of the product (run on Linux only platforms version X.Y.Z etc.)

This is fine as long as features scoped for the app does not require internal changes done within get_ip_address()too.

This is all cool, but 2 weeks later, your team decides that this app needs to run on MacOS as well.

Step 2: Add MacOS support

You decide to run the same code on a Mac.

➜  Desktop python get_ip_address.py
<STACK DUMP HERE>
OSError: [Errno 2] No such file or directory

uh oh… I don’t think this implementation works on MacOS. There’s no ip addron Mac. Time to look for another library to make it work.

Now I think we can all agree that using a 3rd party library to abstract/delegate the nitty gritty implementation is a lot cleaner, clearer and easy to maintain.

In fact, by the time you require cross platform support, you might already be looking for a solution that’s more analogous to the one above. Back to the MacOS conundrum…

Do I need to refactor at this point? Yes.

Step 1 resulted in technical debt that remains stale and generally won’t slow down development for newer features as long as correctness makes sense.

Step 2, on the other hand, require some refactoring and code change. If your team decides that MacOS support is scoped for the product, you might need to go in to change the implementation before being able to work on that MacOS port. This is technical debt that you’ll have to pay before being able to continue development on new features that depend/extend on the interface itself.

There’s technical debt that can be repaid later with very low interest rates. It rarely slow down development on extensible features, and can be revisited at a later time. There’s also another technical debt that demand settlement right away as any further development/feature implementation is stalled until it’s done. There is time penalty for any development work because it’s strapped down by a “broken” feature in the first place.

I don’t think there’s a silver bullet on how to figure this out as you’re rapidly iterating on a new and untested product. However, as an engineer, it almost always pays off by implementing features as best as you can based on you and your team’s interpretation of the product’s long term vision itself.

Any significant deviations will also mean having to stall as implementation has to cave into product decisions. I was burned on a couple projects that I’ve built in the past due to poor architectural decisions, which only got worse as the product vision changed significantly (and having to refactor around that).

In this example, if I had known of a better way to implement such an interface with relatively similar amount of developer effort than a solution that I think will only work in certain cases, I will attempt to deescalate the risk and go with the former approach. The question becomes harder when there’s a tendency of overengineering by sinking too much development time and effort into a feature.

I’ve learned from past experience that this is a difficult balancing act, but it almost always pays off to be constantly thinking about it, thus not sacrificing too much of either innovation speed or developer time + effort.