Technical Debt is like taking a loan.

Let's say you need an expensive car but can't afford it. One option is to take a loan and pay some interest over time. The tradeoff is that you'll pay more on the final price of the car. It's a compromise, but it can make sense if the loan is paid correctly to not incur too much Compound Interest.

However, there's a difference between taking a $30k loan for a car that is necessary to complete your work and a $700k Lamborghini. You should only put yourself in a debt you're able to pay to achieve things you really need so that you don't have to file for bankruptcy.

Technical Debt is most of the time used as a negative connotation, but it doesn't need to be like that all the time. It's like taking a loan. It will allow you to deliver software faster in adverse circumstances without having to invest enough time to find the best design for a problem.

Martin Fowler nails it when he says:

[…] while you're programming, you are learning. It's often the case that it can take a year of programming on a project before you understand what the best design approach should have been. […]

In other words, programming is like a game of "find the pattern".

Requirements will change often and unpredictably. A good strategy is to start with a really minimal and naive approach for a problem. As more requirements come, you change the existing code to accommodate the new requirements. At that point, you may start to see clearer patterns of how the code should be structured for the domain you're working on, then you refactor towards that. It may turn out the refactor brings value, but sometimes it doesn't, so you refactor again and again in a Feedback Loop.

This is also a paradox.

Traditional organizations assume you have to work towards the best design the first time. However, it's impossible to know what a good design is unless you know all the requirements to be able to see the patterns. That process takes time.

This is a huge constraint in Incremental development.

Even if you don't know all the requirements, you can still write software that works. However, it may not have a design that will accommodate future requirements. In that case, Technical Debt can help you to defer refactoring and move forward until the patterns are clear enough to give you good reasons to refactor.

It's impossible to know what a good design is unless you know all the requirements

For the purpose of this post, let's call a "task" anything that can drive you to write code to solve a problem, either in a micro or macro level. It can be a technical task, requirement, feature, story, epic… doesn't matter.

The design for a task can be iterated and improved upon many times before it's considered done. However, after some time, the law of diminishing returns kicks in and the benefits of improving that design won't be worth the time.

Too much design and abstractions can have negative consequences. Duplication is better than the wrong abstraction.

The mental model of a timeline representing the lifecycle of a task. At the bottom, it shows an arrow pointing from left to right containing the caption “Time”. Above the arrow, there's a block spanning from the beginning of the arrow to the end that represents an “Optimal design”.

While requirements that force you to change the code are coming, it makes sense to keep iterating and improving the design. Once requirements stop and you don't have reasons to iterate any further, it's a good idea to stop investing a lot of effort in it. Instead, leave the code in an optimized state to make refactoring easier.

That means writing code with very little abstraction, simpler transformations, and clear tests to document intentions.

The mental model of a timeline representing the lifecycle of a task. At the bottom, it shows an arrow pointing from left to right containing the caption “Time”. Above the arrow, there's a block spanning from the beginning of the arrow to the end. The block has a black section at the end representing some Technical Debt leftover.

You can make a decision to not leave Technical Debt if you're touching a core part of the system that changes often against something that will never be touched again.

A better design, while also helpful for legibility, it's meant to support future changes. If there's a chance that the code you're writing will never be touched again, why bother to invest a lot of time in it?

In that case, leave some Technical Debt at the end and allow the next requirement to drive a better design. In the future, you'll be in a better position to see the patterns instead of speculating on it.

The mental model of a timeline representing the lifecycle of a task. At the bottom, it shows an arrow pointing from left to right containing the caption “Time”. Above the arrow, there’s a block spanning from the beginning of the arrow to the end. The block represents 2 tasks. The first task added some Technical Debt at the end, the second task drove the developer to clear the previous Technical Debt. The second task has a black section at the end representing some Technical Debt leftover to facilitate the next design decision.

This looks great, but here's some bad news: there's no silver bullet.

The only way this can work is when all developers working in the codebase are aligned to the same level of skill and discipline. If they're not, it will be very hard for them to understand the intentions behind the code and keep up with the right decisions.

In real life, it's rare to find teams where everybody is completely aligned. This is where Mob Programming and Pair Programming can help to shape the culture and improve collaboration.

Keep in mind nothing can be used as an excuse to write bad code.

A cartoon showing one man in the right sweating in an effort to try to drive a bicycle with square wheels. The caption reads “Can't stop. Too busy!!”. In the left, there's a person holding a rounded wheel. The person in the left is looking at the man in the right, dazzled. (source)

You can only use Technical Debt in your favor if the team is aligned, collaborative and disciplined

A common problem with this approach is that developers tend to leave too much Technical Debt behind instead of just enough to facilitate the discovery of patterns in the future.

That can make things worse.

The idea is to facilitate the next design decision, not to make it harder.

The mental model of a timeline representing the lifecycle of a task. At the bottom, it shows an arrow pointing from left to right containing the caption “Time”. Above the arrow, there's a block spanning from the beginning of the arrow to the end. Half of the task is blacked out with Technical Debt leftover.

If you leave a lot of Technical Debt, nobody will have time or willingness to fix it later. It will just drive more broken windows.

Don't sacrifice Clean Code.

Take a conscious decision in a way that it can allow you to move forward while supporting future refactoring. It should only be done in a way the code and the tests can expose the smells behind it.

Debt is not always bad.

If you didn't have the option to borrow money you wouldn't be able to buy a house or a car.

Same goes for Technical Debt.

Make sure you iterate on the design of your tasks incrementally and create quality tests, otherwise you won't be able to refactor with confidence.

Make sure you're refactoring in a way that can help your old code to accommodate the requirements you have, nothing else.

Refactor for the past you know, not the future you don't

The role of a programmer is not only to write code but to put some sense into a world full of logical nonsense.

It's a job of "find the patterns".

Watch out for that "aha" moment and leave the code in a state that will make it easier for someone else to find it.

Perhaps in the future, we'll live in a world where these practices are the norm.

Unfortunately, as an industry, we're far from that.

It's up to you to start changing it.