Levix

Levix's zone

x
telegram

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

The concept of technical debt was first introduced by Ward Cunningham, referring to the cost of accelerating the software development process for short-term efficiency gains, at the expense 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 early stages of a project. However, once you take out a loan, you must pay interest. For technical debt, this corresponds to the 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 process of refactoring, where you invest time to improve code quality in hopes of speeding up future development.

Like financial debt, technical debt can also be a useful tool. Sometimes the short-term gains can outweigh the long-term impacts. However, just as excessive financial debt can lead to bankruptcy, accumulating too much technical debt can slow down your product development to a crawl or even bring it to a standstill.

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

However, as the application matures and grows, the development speed will inevitably slow down. On a poorly designed product, the development speed may decrease rapidly, and even with a sufficiently elegant design, the speed will still gradually decline over time. This is because the more code you add to the application, the slower development becomes. Therefore, I believe all code, in essence, constitutes technical debt.

Adding New Assumptions Increases Technical Debt#

As the application continues to improve, a series of foundational assumptions will gradually form. When you first start a new project, there are no preset assumptions in the codebase since there are no features yet. This makes adding new functionalities very straightforward, just like directly implementing features. However, once your project has its first feature, every subsequent step of development must consider the limitations that previous features impose 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 designed to simplify the registration and check-in process for a local networking event—Mobile Monday Tokyo. At that time, they only needed a simple registration process, allowing users to register for and cancel events.

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

We built the limited-participant events based on an assumption: people can register for events. But we also needed to decide how to handle registrations when an event is full. The simplest method is to prohibit registration when full, but we believed this would disappoint those wanting to attend and wouldn't provide feedback to organizers about the event's popularity. So, we introduced a waitlist mechanism: if someone wants to register for a full event, they will be added to the waitlist, and if a participant cancels, the first person on the waitlist will take their place.

The next feature we added was prepaid registration for events. If our only assumption was "people can register for events," implementation would be straightforward, but we had to consider other existing assumptions.

Depending on the event, organizers may or may not allow prepaid participants to cancel. Additionally, they might want to set cancellation policies that provide different refund rates based on the time of cancellation. Since canceling prepaid registrations involves many special cases, and most organizers do not want to make this process too easy, we decided not to allow participants to cancel their prepaid registrations independently. Instead, participants need to contact the organizers, who will decide how to handle it.

Moreover, because the number of registrations for an event is limited, we need to ensure that only those who genuinely have a chance to attend can register with prepaid options. To achieve this, we divided the registration process into two steps: first entering registration details, then making payment. If the event is full between the time the user fills out the initial form and submits it, that user will be added to the waitlist.

We also had to handle cases where users submitted the initial form but did not complete payment. To address this, we introduced a mechanism to automatically cancel these unpaid reservations after a certain period.

Another situation to consider is when someone from the waitlist is confirmed. For events requiring prepaid registration, we cannot immediately issue tickets; users must return to the website to complete payment.

As you can see, with the continuous addition of new features, considering and resolving issues based on existing assumptions becomes increasingly complex.

Features Can Bring Negative Value#

For a feature to add value to a product, it needs to be genuinely useful to users. When the technical debt a feature incurs exceeds the value it adds to the product, that feature effectively has negative value.

Localization is a feature that often generates negative value. At its simplest level, localization involves translating your application into multiple languages. Beyond translation work, this process involves more challenges. Even considering the simplest form of localization—maintaining a dictionary of language-specific strings.

As a preliminary step, this means your application can no longer contain hardcoded text, adding an extra layer of abstraction. Although this layer of abstraction does not add much extra work, developers still need to be mindful of it at all times.

You also have to incorporate the translation step into the development process. Considering that developers may not be native speakers of all supported languages, they cannot complete the text portion independently and must rely on translators. This undoubtedly slows down all future development processes slightly.

If localization can generate significant value, then this extra work is not an issue. However, I often see applications undergoing sloppy localization in hopes of attracting users from other countries. But this is not an effective way to work. Unless you are willing to fully commit to localization and market promotion in supported language regions, localization will not bring real value. As a result, you will end up with a feature whose technical debt exceeds its inherent value.

Code Itself Does Not Carry Value#

As developers, we can easily fall into the illusion that writing code is creating value. However, the value of software lies in its utility to users, not in the quality of our code. Even poorly written code that implements useful features is far more valuable than beautifully written code that accomplishes useless tasks.

For this reason, we need to ensure our development work focuses on valuable features. While traditional development processes often assume there is an all-knowing product owner who can accurately assess the value of each feature, the reality is often not so.

I hope you have collaborated with other stakeholders and deeply understood the value of the work at hand. But if you do not believe this, you should question why they think this feature is valuable and whether any validation work has been done.

Ensuring that the features we implement have value reduces the likelihood of their technical debt becoming an unbearable burden.

Once a Feature is Added, It Likely Remains Permanently#

One reason 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 when it becomes clear afterward that the feature did not perform as well as we expected, the common practice is to take no action. This is partly due to a misjudgment of sunk costs, often accompanied by a hope that even if the feature is not currently used, it may be useful in the future.

However, there are also valid reasons for inaction. Removing a feature also incurs costs, including the development work required to eliminate it, and may also lead to customer dissatisfaction. Therefore, once a feature is added to a product, it almost always exists.

To Avoid Technical Debt, It’s Best Not to Write Code#

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

For example, I run a job board that helps international developers find work in Japan. Initially, I just randomly posted job information I came across, but as I heard more success stories, I thought companies would be willing to pay for this service. At the same time, I wanted to perform some basic spam screening before applications were sent to companies.

To achieve this, I started building a screening system using Ruby on Rails. However, after about a day of development, I realized that creating a system that performed better than having candidates send emails was quite complex. I needed to replicate all the functionalities of email: attachments, communication between candidates and companies, etc. More critically, I needed to ensure the system ran reliably, monitoring logs, and so on.

However, what I really needed was a way to conduct moderate reviews before emails were sent to companies. Therefore, I chose not to continue down that path and instead used the group feature in Google Apps to set up an email list for each company. While this may not be the best technical solution, the value I created was not solely about the technology itself. Instead, I should have spent my time helping connect companies and candidates.

Working Within Existing Frameworks to Implement New Features#

When we add new features, one way to reduce technical debt is to work within existing assumptions or constraints rather than adding new assumptions. Let’s illustrate this with our practice in Doorkeeper.

Event organizers can send messages to participants. We suggest that organizers use this feature to send event reminders to participants. However, some organizers never send such reminders, perhaps because they forget or are too busy. Meanwhile, some organizers send customized reminders that include specific instructions for the day of the event.

Our goal for this new feature is to ensure that every participant always receives a reminder without relying on the organizer to send messages manually.

The simplest way to add an automatic reminder feature seems to be to automatically send an email to participants the day before the event. This implementation is straightforward but does not adequately consider those organizers who need to send temporary instructions. If organizers also need to send additional messages as supplementary reminders, participants may receive multiple emails about the same thing, leading to a less than ideal experience. To address this issue, we could allow organizers to customize the reminder content.

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

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

By building on existing features rather than creating a brand new independent feature, we found a better solution. It not only provided a better user experience but also generated less technical debt.

Summary Review#

So to quickly recap, since adding more code to a product slows down development speed, we should view all code as a form of technical debt. To ensure 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, protecting it from immature feature designs. I know this is not easy, but by ensuring that the products we create are valuable, rather than just continuously producing code, we can achieve greater benefits.

Original 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.