- The hidden causes of technical debt
- Balancing technical debt in startup growth
- Guarding against technical debt
Technical debt can have a negative impact on the value of our software or business, much like bugs. However, there are two major differences between them. Firstly, technical debt is often invisible and hard to measure. Secondly, the influence of technical debt is usually indirect. Its scope can also be very broad and can exist in places other than the code itself. Because of these two aspects, it can be more challenging to address than simply fixing any bugs that arise.
Knowing how to handle technical debt is not always easy, but failing to address it can have terrible consequences. To name a few:
- making the application more prone to bugs,
- exposing security vulnerabilities,
- increasing the overall time to market,
- degrading developer experience,
- increasing onboarding time of new developers.
Technical debt also has a psychological side to it. The negative aura it creates can be compared to broken windows in a building. It's much easier to justify breaking another window in a building that already has some of them broken. The same can be observed with technical debt.
The mental barrier for using workarounds when other workarounds or hacks are already in place is much lower than it otherwise would be. This can lead to a rapid degradation of the situation. That’s the reason why technical debt needs to be constantly tracked and threaded carefully in areas where it's the worst.
Technical debt is also very contextual. In this article, I would like to explore the reasons for how and when technical debt is born and the possible ways of prevention, primarily in the context of startup businesses.
The hidden causes of technical debt
As I mentioned before, the scope of technical debt can be very broad. For example, we could be talking about:
- lack of documentation,
- misuse of or high dependence on external services,
- disorganization of the code,
- badly architected software,
- lack of tests,
- violation of good software principles,
- code optimization problems.
Each one of these examples can have a different root cause, but I would like to focus on the most common ones.
The first and the most obvious reason is writing "bad" code. This could mean a lot of things, for example:
- using non-idiomatic solutions for the technologies in the project,
- writing non-performant code,
- violating common software design principles,
- making the code not readable enough or not documenting it when needed,
- implementing things too fast in too little time.
Despite the aim to write the best code possible, sometimes we lack the proper knowledge to do so. This is what we call inadvertent technical debt. If the entire developer team is not experienced or skilled enough to tackle the project at hand, the growth of technical debt is inevitable.
Another example of inadvertent technical debt is an incorrect implementation due to human error. What can be causing it? Two common examples are vague requirements and a lack of technical specifications. Such cases are often undetected during peer code review and make themselves clear further down the line. They often cannot be afforded to be fixed due to time constraints.
Is it true that tight politics of peer code reviews and precise specifications will prevent any technical debt from arising? The answer is still no. Sometimes, technical debt is deepened on purpose.
Compromises between time and quality
A lot of reasons for doing so are specific to the nature of startups. The most common one is time, or rather, the lack of it. When a feature or a fix needs to be delivered on time and on budget, it is sometimes necessary to take shortcuts. Perhaps we won’t need the feature in the future, or perhaps it will require an overhaul anyway. In cases like these, a compromise has to be reached, either on the business level or on the technical level.
Another reason is that the ROI (Return On Investment) of an idiomatic solution may just be too low to be worth it. Spending too much time on a proper implementation without any plans for the future can be a huge risk and an unnecessary time-sink. In cases like those, it may be better to choose the path of least resistance. The minimum viability for users is often enough.
This concept may also be prominent on a smaller scale. Certain solutions may require overengineering for a problem that can have a suboptimal but much simpler solution. For example:
- premature optimization,
- mindless implementation of complex functionalities that may not be needed or can be simplified,
- premature abstraction,
- implementing things in advance that were not specified yet or may not be needed in the future.
Just make sure you are aware whenever you would be cutting corners on the security of your application. If the solution you've chosen is an external service, also verify you are not completely vendor-locking your application.
But in my experience, the biggest reason why technical debt arises is the volatile nature of startups. When the functionalities of the application are unclear or constantly changing, it may be straight-up impossible to know what the proper solution should look like. Drastic deviations from the planning phase can cause technical debt to accumulate quickly.
Balancing technical debt in startup growth
In some circumstances, technical debt is more common and acceptable than in others.
As an example, technical debt can have a significant impact on open-source projects. It can greatly affect the usability of a product, limit the possibility of external contributions, and erode the trust of its users. As a result, the project may become less popular and have lower chances of success.
Technical debt should rarely occur in software that has clearly defined requirements and established funding. Under proper technical leadership, a well-planned architecture of the entire solution and micro-architectures of its functionalities should minimize the technical debt. And any that does arise should be negligible or manageable.
This poses a question - are there situations where technical debt is actually acceptable and occurs naturally? I am positive that there are multiple different sets of circumstances where this is true, but one that I would like to explore and focus on is startups.
For startups, it's very important to consider the moment you are in with your business. Be careful when taking advice, as context matters a lot here. This is why this section is divided into two:
Before product-market fit
Ask yourself, are you building a prototype or an MVP? Are you trying to materialize your idea into any form and testing a concept? Are you cutting corners to move as fast as possible? Are you okay with throwing everything away and rewriting the product from scratch, regardless of whether your notions are confirmed or not? If so, then you're building a prototype.
By definition, a prototype is not supposed to become a fully-fledged product on its own. Once you build a prototype and confirm your thesis, most of the time, you will need to rewrite everything anyway or start a new prototype. This means that in such an environment, the existence of technical debt is not only completely natural but actually encouraged. The faster you get your prototype done, the faster you will be able to move in your journey of developing a successful startup.
A different part of the journey could be an MVP. Building an MVP is still a phase of experimentation, verification, and exploration. At this stage, technical debt is still acceptable, but in a different form. Unlike a prototype, it is designed to be iterated upon and improved continuously rather than scrapped and rewritten. This is why technical debt should not be encouraged anymore and should be avoided if possible.
However, it still occurs, but more commonly as inadvertent technical debt. Due to the iteration mindset, the specifications of the system are rarely known or have to be implemented in steps. Things that we are planning for now may no longer be true in the near future. This can happen even when the initial implementation is very clean and thought-through.
After product-market fit
Whether your startup is in this phase or not will depend on your own personal metrics. But in my opinion, one common characteristic is that the tolerance for technical debt is very low.
At this point, introducing any kind of technical debt can be a red flag of the project's health. Time shouldn't be your biggest constraint now, so there should be no incentive to cut corners. This should keep the introduction of new deliberate technical debt close to zero.
Even inadvertent technical debt should not occur often. New features or changes should be thought out and thoroughly defined. This minimizes the uncertainties and risks that cause inadvertent technical debt to occur. Justified changes in the architecture should also be prepared in advance.
Building something upon a fundamentally flawed product can have catastrophic consequences. Make sure it is not neglected as, at some point, it may be faster to rewrite the application from scratch rather than to fix the technical debt. An ingrained process for reducing it will go a long way in ensuring that your product stays healthy.
As we already know, we may not be able to predict how our business logic will evolve. So, how can we minimize this risk?
Guarding against technical debt
Minimizing the impact of technical debt is a very complex topic. To keep things pragmatic, I would like to focus on two areas I believe to be the most important to prevent the majority of technical debt.
More often than not, your team will consist of multiple developers. It is very important to include at least one person who is qualified to drive the technical aspects of the project and make sure the proper code quality is maintained.
The next important thing is to employ their expertise, which usually happens at the process level. The expert's involvement is crucial during planning meetings, refinements, business decisions, and, most importantly, code reviews and mentoring of other developers.
Code quality standards should be established and formalized as early as possible. They should also evolve continuously along with the project. These can involve things as simple as linting rules or file organization conventions. It’s best when most of this verification is automated, but the most important step is to maintain those standards.
Peer code reviews play a vital role in this. Any change in the software should always be verified by others, especially by someone who is responsible for protecting high code quality. In the end, software development is a team effort. As many team members as possible should participate in peer code reviews. It brings many more benefits than just the prevention of possible technical debt.
Assuming we have the appropriate expertise in the project, is there any way to guard ourselves against technical debt caused by uncertainties? Not completely, but a very powerful method to mitigate most risks can be one simple guideline.
The ETC principle
A very simple yet powerful principle is the "easy to change" principle (ETC for short). Exactly as the name suggests, we should create solutions that are easy to extend, modify, or remove after their implementation. This principle should especially be considered whenever we are in doubt or the future of any part of the system is unclear.
Here are some common day-to-day situations and tips on how to handle them:
Breaking changes in schemas
If your application is already in production, try to remind yourself of these questions. Will breaking changes require a migration? Can these migrations be handled gracefully? Is this the last breaking change in this area?
If the answer to all of these is a profound yes – introducing a breaking change should be safe. Otherwise, maybe it would be better to extend a schema incrementally and deprecate a part of it instead.
Database schema design
There are many principles of good database schema design, but the ETC principle can also be applied here. It can be a guide whenever you are facing a crossroads and you're unsure what the shape of your database will have to look like in the future.
You can consider which structure will be the easiest to pivot from. Especially be mindful if you have a use-case to have a tree structure in your data. In cases like those, maybe a flattened structure would work better. Or, in your case, it might be the other way around.
At times, we are forced to take shortcuts to be able to deliver something on time. The ETC principle comes in handy here, too. Make sure that such code is not ingrained deeply within your codebase. If possible, isolate it in a module, component, function, class, or any other data structure. Make it so that it is low-risk to rip it out or refactor it in the future.
This approach has the additional advantage of making things easy to test and usually promotes good code readability.
Integration with third-party services
How many uncertainties are there? Will this service cover all of the current needs? What if these needs change? Will the service allow us to adapt, i.e., how flexible/extensible is this service? If push comes to shove, how painful will it be to replace this integration with something else?
Deciding on services in the sea of endless possibilities is not easy. Sometimes, popularity can be a good landmark. Other times, you may be looking for a very specific service that may not be that well known. What seemed like an obvious choice at the start may turn out to not be what you need. Hopefully, these questions will help you narrow down the possibilities.
Complex code and coupling
It's very important to consider the boundaries of code within your project. A leaky architecture with no clear boundaries can become a huge pain point in the future. Always ask yourself, even without thinking about the main implementation, if this file should depend on that other file. It is difficult to refactor code that has a lot of dependencies, circular dependencies, or leakage of global context.
This is why it's best to keep code simple and decoupled. Keep your utility functions and constants without dependencies. Scope your global context usage to only the most crucial parts. Use dependency injection where it makes sense.
Technical debt is an inseparable aspect of software development. How it gets born and how it should be treated depends on the context of each project. In startups, technical debt can even be a sign of a healthy approach. Here are the most important takeaways I've collected from working with startups:
- If there are any uncertainties, consider the ETC principle.
- Isolate any code you think is at risk of changing.
- Think about Plan B when picking an external service.
- Make sure your code is simple and decoupled wherever it makes sense.
While I didn’t explore ways to handle technical debt in the context of more mature and bigger projects, here are a few ideas that could be used both in startups and non-startups:
- Periodically set aside time to refactor and address any existing technical debt.
- To make the implementation of new functionalities smoother, prioritize the reduction of relevant technical debt during the planning stage.
- Use a documentation system to track technical debt items. Make sure they are not only left as TODOs in the code.
- Conduct regular retrospectives to evaluate the impact of technical debt on the development process and identify areas for improvement. Frameworks like DORA or SPACE can be useful in this regard.
There are still more areas that I haven't explored in this article. For example, the in-depth prevention of technical debt or deciding on when and how to refactor. I hope the information contained here has made you more aware of the existence of technical debt and pointers on what to look out for. In the end, every project is unique, so a pragmatic approach to technical debt is very important.
Software Architect & Tech Lead at Vazco
Software engineer with a passion for front-end development. Likes making the users' and developers' lives easier. In his spare time, he enjoys losing himself in music and video games, especially of Eastern origin. Speaking of the East, he is enthralled by Japan and the Japanese culture. He achieves 120% work efficiency thanks to his little, winged helper despite communication differences 🦜