Seven Learnings About Technical Debt
“It is not enough for code to work.”
― Robert C. Martin
Every developer knows the situation. In order to be faster and meet deadlines, we sometimes need to sacrifice code quality. Every time we do this, we accumulate cruft or technical debt. The amount of technical debt comes with an interest for every new feature — the extra effort needed to get it done. This interest accumulates fast and it can be seen as financial debt. A small debt won’t break the bank, but the more debt one accumulates, the slower feature development becomes until the decision for a full rewrite comes, which is the equivalent to bankruptcy. For more information, check out the many excellent resources out there.
The goal of this article is to show the learnings about technical debt we made while working on a huge code base and revamping major parts of it. The codebase belongs to a company which is growing fast and develops a high-quality medical app. Obviously the experience can be very different, when working in a very small team, but these learnings can be applied everywhere — at least to some degree.
Learning 1: Be Careful With Prototypes
One of the most important buzzwords in agile software development is the MVP (minimum viable product), which is strongly related to creating prototypes, fast. I don’t want to discourage the development of prototypes, but it is important to clarify early on what the goal of the prototype is. Broadly speaking, we can define two types of prototype:
- Throwaway prototypes: The goal is to try things out, be it of technical nature or to show iterations of the UX to the user. As the name suggests, the code has to be thrown away afterwards. Ideally mark all throwaway code. Add a huge disclaimer on top of every file, so that nobody gets the idea to ever merge this to production.
- Evolutionary prototype: Start out with unclear requirements and iterate slowly to get to the final product. In this case, try to feel out where the biggest uncertainties are and work around them. Here, a stable foundation is key. Of course the foundation can’t be as stable as when all requirements are known upfront, but keep iterating over your architecture like everything else and try to anticipate the biggest areas of change beforehand. To figure out these areas, a strong alignment with the design and product department is needed.
The problem is, that often the throwaway prototype evolves into an evolutionary prototype, which can be unintended. In this situation, it is very difficult to explain to stakeholders that this is not a finished product, but still needs a lot of work around the edges. Our amazing Agile Coach Lead, Rahul wrote a great article about this topic, which explains these dynamics very well.
Learning 2: Raise Concerns Early
When the technical health of the product is bad, there is sometimes a degree of resignation. “Yeah, it would be important, but that’s just how it is.” Adding to that, developers are often not known for their people skills. Still, in these situations, these skills are needed to ensure the long-term success of the project. It is extremely important that degrading technical health is communicated to other stakeholders, which could be product managers, developers from other teams or the customers themselves. Delaying deadlines can be very painful for the whole company and it sometimes might not be even possible, but the cost in the long-run is usually much higher.
The discussions can become hard and pushback from the business and product side will come for sure. Anyway, these different viewpoints are needed to reach a common understanding. In these situations, remind yourself of the common goal of the team and commit together on it. By evaluating the impact on the short-term and the impact on the long-term you can come up with a plan together on how to tackle the technical debt. In this process, trust between the involved parties is critical. Everybody is, or should be, an expert in their field and not every small decision should be questioned.
But, by being very objective (more on that later) common suspicions between parties can be mitigated. And when in the end, the deadline can’t be delayed and the technical debt can’t be paid back right now, the technical health will be on the mind of the whole team. And because a common goal was defined, the technical debt can be paid off in less turbulent times. Which brings me to the next learning…
Learning 3: Make it Visible
Technical Debt should be handled the same as user stories, improvements or bugs. Tickets need to be created, they should become part of the backlog and prioritized like other tickets. It is important to always give a rationale as to why this improvement is important. This doesn’t only make the issue graspable to less technical team members, it also helps oneself to be more aware about why this needs to be tackled.
It is important to estimate tickets paying off technical debt. As developers tackle these tasks with a great deal of eagerness and likely some optimism, they tend to underestimate them. On top of that, paying off technical debt can come with many uncertainties. Touching code that works, but nobody knows why it works, can lead to many unexpected side effects. (Probably every developer can tell a great story here.)
Also, group tickets in milestones! Paying off technical debt makes every developer happy, of course, but it can also be a long and tedious process. Milestones can provide healthy checkpoints and relieve pressure, which can arise when working on “invisible” things. The ideal milestone can be measured and includes reasoning.
Some good and less good milestones for technical health:
- Good: Module X must follow the module guidelines, because the code becomes reusable and our build speed will be X % faster.
- Good: X % of Presentation and Business logic are in the ViewModel/UseCases and covered with tests, because we need to be sure that data is presented correctly.
- Bad: We want to introduce UseCases.
Learning 4: Visualize and Explain it
When it comes to paying off your technical debt, it can be very difficult to convey to non-technical stakeholders why this is needed right now. Making it visible is the first step, but it is even better when you can explain the issues and solutions in simple terms. Powerful tools here are metaphors, diagrams or metrics.
An example I like very much is the Poka-yoke concept. It is a Japanese term which means mistake proofing. In the real world, you would encounter it when you connect a cable to a port. These are usually designed in a way that cables can only be plugged in ports which can also receive the data. Applying the concept to programming, we can convey why we need to convert our primitive types to strong types.
Learning 5: Be Pragmatic
When looking at legacy code, it is easy to find all places which are ugly and can be improved. As proud developers, we would like to improve every code smell we see and polish our code as much as possible. This is a very good quality for a developer and shows that we care, but it can also backfire. Some code might not be the best and horrible to look at, but it works. So sometimes the better way to approach it is to swallow the bitter pill and live with the legacy code.
To know when to tackle the technical debt VS when to let it be, we can ask ourselves a simple question: “What is the actual cost of still having this debt?” Examples are:
- When this error-prone class is not covered with tests, we will encounter a lot of bugs in the future. (Strong candidates here are classes which caused trouble before and which never got sustainable improvements.)
- When we don’t separate these concerns, it becomes harder to change the database later.
- When we don’t extract this module, we will struggle with slow build times.
- When we don’t make our architecture reactive, we will increase the technical debt with every new feature.
This simple exercise doesn’t only make it clearer to the outside why paying off the technical debt is necessary, but also makes sure that the biggest pain points are tackled in the most effective way. In the process, you could realize that this very ugly class you are dying to rewrite will not be touched in a long time, as there are no plans to add or change the functionality of it, and therefore, you could live with the technical debt a bit longer.
When approaching technical debt always think about the big picture and the high-level architecture first. Structural problems in your architecture can be very hard to fix at a later point, so prioritize these. Fixing all the little code smells and extracting functions to their perfect granularity are worthwhile goals too, but don’t dwell on them and rest assured that this can always be improved at a later point, as long as the architecture is in a decent shape.
Having said all that, it is easy to underestimate technical debt and it’s likely that it will derail you in the long-run. So it is always worthwhile to find time to tackle technical debt, just also be aware of other constraints and always focus on what’s most important right now.
Learning 6: Define Quality and Measure it
As programming has always lived in the intersection of art and science, it is important to mention that code quality can be something very subjective. That’s one of the reasons why there are so many programming languages. Every developer has their own taste to some degree, which is also reflected in their code style. But this doesn’t mean we can’t make our journey to better code quality more objective.
First things first, formatting and code styling should be checked with automated tools such as lint. Discussions about whether this semicolon is needed or not should never happen during code review. When they happen, think whether your static checks are lacking something. On top of that, guidelines which are agreed upon among a majority of developers in the company can make the code base much more consistent and simplify discussions in code reviews.
Going even further, a great way to assess code quality is to define quantitative metrics and measure them. Every project will have its own metrics, but metrics you could look for are code coverage, build speed or cycle time. This topic is worth a blog post on its own, stay tuned for it.
Learning 7: Accept it
After all these learnings, we come to the final stage: Acceptance. There is no such thing as perfect code and, as all projects are subject to change, be it of technical nature or of shifting requirements, it is important to remember that there will always be room for improvement for our code base. So, in the end we need to see technical debt similar to financial debt. Sometimes debt might be unavoidable to reach certain goals. At the same time, we can’t accept that it will always stay there, as this would mean a certain bankruptcy in the future.
Whenever there is a chance to make our code more future-proof, we should take it. Improving code quality is an iterative process which should be tackled with an agile mindset. It should be always kept visible in the backlog and tackled before it becomes a problem. At mySugr, we devote 10% of our development time to improving code quality. Every month, the Android department takes two and a half days to focus strictly on technical improvements, which is always a great opportunity to clean up our technical debt.