Levix

Levix's zone

x
telegram

All written code will eventually become a technical burden (technical debt).

Ward Cunningham first proposed the concept of technical debt, which refers to the process of accelerating software development to achieve short-term efficiency gains, but at the cost of slowing down future development speed.

This is similar to taking out a loan from a bank. A loan allows you to complete something faster than usual. Technical debt is like the momentum you gain during the initial development process of a project. However, once you take out a loan, you have to pay interest. For technical debt, this is equivalent to a slowdown in future development speed. When you repay the principal of the loan, the interest you need to pay also decreases. This is similar to the refactoring process, where you invest time to improve code quality in order to speed up future development.

Just like financial debt, technical debt can also be a useful tool. Sometimes the short-term benefits you can gain outweigh the long-term impact. However, just as too much financial debt can lead to bankruptcy, accumulating too much technical debt can slow down or even halt your product development.

If you have experience creating a new application from scratch, you must have experienced the joy of smooth progress without any technical debt. In the early stages of development, you can iterate quickly to add new features without worrying about impacting existing users, and you can fully focus on implementing new features.

However, as the application matures and grows, the development speed inevitably slows down. On a poorly designed product, the development speed may quickly decrease, and even if the product is designed elegantly, the development speed will still gradually decrease over time. This is because the more code you add to the application, the slower the development becomes. Therefore, I believe that all code, fundamentally, constitutes technical debt.

Increasing new assumptions leads to an increase in technical debt#

During the process of continuously improving an application, a series of fundamental assumptions gradually form. When you start a new project, there are no features or presets in the codebase. This makes adding new features very simple, just like directly implementing the functionality. However, once your project has the first feature, every subsequent step of development must consider the limitations of the previous features on future development work.

Take the community event platform Doorkeeper that I created as an example. It helps organizers attract participants and manage registrations. Initially, Doorkeeper was only designed to simplify the registration and check-in process for a local network event, Mobile Monday Tokyo. At that time, they only needed a simple registration process and users could only register and cancel for events.

As we expanded Doorkeeper to accommodate the needs of more organizers, we discovered that many events had a fixed participant limit, which was not considered by Mobile Monday before. To meet these new requirements, we decided to add a feature to limit the number of participants.

We built a limited capacity event based on an assumption: people can register for events. But we also needed to decide what to do when an event is full and someone tries to register. The simplest approach is to directly prohibit registration when the event is full, but we believe this would disappoint people who want to participate and would not provide feedback to organizers on the popularity of the event. So, we introduced a waiting list mechanism: if someone wants to register for a full event, they will be added to the waiting list, and if a participant cancels, the first person on the waiting list will take their place.

Next, we added the feature of prepaid events. If our only assumption was "people can register for events," implementing it would be very simple. However, we also had to consider other existing assumptions.

Depending on the event, organizers may or may not allow prepaid participants to cancel. In addition, they may want to set cancellation policies, such as providing different refund percentages based on the cancellation time. Since canceling prepaid registrations involves many special cases, and most organizers do not want this operation to be too easy, we decided not to allow participants to cancel prepaid registrations on their own. Instead, participants need to contact the organizers, who will then decide how to handle it.

Furthermore, since the number of registered participants for an event is limited, we need to ensure that only people who have a real chance of participating can prepay. To do this, we divided the registration process into two steps: entering registration details first, and then making the payment. If the event is full between the user filling out the initial form and submitting it, the user will be added to the waiting list.

We also had to deal with the situation where a user submitted the initial form but did not complete the payment. For this, we introduced a mechanism to automatically cancel these unpaid reservations after a certain period of time.

Another consideration is when someone is promoted from the waiting list. For events that require prepayment, we cannot immediately issue tickets and need to ask the user to return to the website to complete the payment.

As you can see, with the addition of new features, considering and solving problems based on existing assumptions and constraints becomes more complex.

Features can bring negative value#

In order for a feature to add value to a product, it needs to be truly useful to users. When the technical debt incurred by a feature exceeds the value it adds to the product, the feature actually has negative value.

Localization is a feature that often generates negative value. At its simplest level, localization means translating your application into multiple languages. In addition to the translation work, this process involves more challenges. Even if we only consider the simplest form of localization - maintaining a dictionary of language-specific strings.

As a start, this means that your application should no longer contain hardcoded text, which adds an extra layer of abstraction. Although this extra layer of abstraction does not add too much additional work, developers still need to be mindful of it.

You also need to add the step of translation to the development process. Considering that developers may not be native speakers of all supported languages, they will not be able to independently complete the text part and will need to rely on translators. This undoubtedly slows down all future development processes.

If localization can generate significant value, then this extra work is not a problem. However, I often see some applications with sloppy localization, hoping to attract users from other countries through this. But this is not an effective approach. Unless you are willing to fully commit to localization and market promotion in the supported language regions, localization will not bring real value. In this case, you will end up with a feature that incurs more technical debt than its own value.

Code itself does not inherently have value#

As developers, we are prone to the misconception that writing code is creating value. However, the value of software lies in its usefulness to users, not the quality of our code. Code that is not perfect but implements useful functionality has much greater value than beautiful code that accomplishes useless tasks.

For this reason, we need to ensure that our development work focuses on valuable features. Although traditional development processes often assume the existence of an all-knowing product owner who can accurately judge the value of each feature, the reality is often different.

I hope you have collaborated with other stakeholders and deeply understand the value of the work you are doing. But if you don't think so, then you should question and try to explore why they think this feature is valuable and whether any validation work has been done.

By ensuring that the features we implement have value, we reduce the possibility of them becoming an unbearable burden of technical debt.

Once a feature is added, it will likely exist permanently#

One of the reasons why we need to be highly vigilant about features that do not provide sufficient value is that once a feature is added, it usually remains permanently.

Even if it becomes apparent afterwards that the feature did not perform as well as we expected, the common practice is to take no action. This is partly due to misjudgment of sunk costs, often with the hope that the feature may be useful in the future, even if it is not currently being used.

However, there is also a valid reason for not taking action. Removing a feature also incurs costs, including the development work required to remove the feature, and it may also cause dissatisfaction among customers. Therefore, once a feature is added to the product, it almost always remains.

To avoid technical debt, it's best not to write code#

The most reliable way to avoid technical debt is to not write code in the first place. As developers, our initial instinct is to solve problems by writing code, but this is not always the best strategy. Many times, we need to restrain this instinct.

Take my own example, the job board I run, which helps international developers find jobs in Japan. Initially, I randomly posted job listings that I came across, but as I heard more and more success stories, I thought companies would be willing to pay for this service. At the same time, I wanted to have a basic spam filter in place before applications were sent to companies.

To achieve this, I started building a filtering system using Ruby on Rails. However, after about a day of development, I realized that creating a system that performs better than sending emails to candidates is quite complex. I would need to replicate all the functionality that emails have: attachments, communication between candidates and companies, etc. More importantly, I would need to ensure the stability of the system, monitor logs, etc.

However, what I really needed was a way to moderately review emails before they were sent to companies. So instead of continuing down that path, I chose to set up a mailing list for each company using the group feature in Google Apps. Although this was not the best technical solution, the value I created was not solely in the technology itself. Instead, I should have spent my time on helping establish connections between companies and candidates.

Implement new features within existing frameworks#

When adding new features, one way to reduce technical debt is to work within existing assumptions or constraints, rather than adding new assumptions. Let me explain using our practice in Doorkeeper as an example.

Organizers of events can send messages to participants. We suggested that organizers use this feature to send event reminders to participants. However, some organizers never send these reminders, perhaps because they forget or are too busy. At the same time, some organizers may send customized reminders, including specific instructions for the day of the event.

Our goal as the creators of the new feature was to ensure that every participant always receives reminders, without relying on organizers manually sending messages.

The simplest way to add automatic reminders seems to be to automatically send an email to participants one day before the event. This implementation is straightforward, but it does not take into account organizers who need to send ad-hoc instructions. If organizers also want to send additional messages as supplementary reminders, participants will receive multiple emails for the same thing, which would not provide an ideal experience. To address this problem, we can allow organizers to customize the reminder content.

Considering the need for customizable reminders, we realized that reminders and messages are essentially the same. If we can automatically schedule a message to be sent at a specified time, then organizers can modify this message or even disable the feature completely.

At that time, we did not have the functionality to schedule messages to be sent at specific times. However, allowing organizers to schedule messages to be sent at specific times is also a useful feature in a broader context. More importantly, by keeping reminders as regular messages, we can show organizers the usual related reports, such as how many people opened and clicked on the messages.

By building on top of existing features rather than creating a completely new standalone feature, we found a better solution. It not only provides a better user experience but also incurs less technical debt.

Summary#

So let's quickly recap. Adding more code to a product slows down development, and we should consider all code as a form of technical debt. To ensure that development does not gradually stagnate, we need to ensure that the code we write creates more value rather than debt.

As developers, we should be guardians of the product and protect it from immature feature designs. I know this is not easy to do, but by ensuring that the products we create have value, rather than just continuously producing code, we can reap greater benefits.

Original article link: https://www.tokyodev.com/articles/all-code-is-technical-debt

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.