All code is technical debt
Technical debt, as originally coined by Ward Cunningham, is the idea that you can gain a temporary speed boost by rushing software development, at the cost of slowing down future development.
This acts like a loan. With a loan, you can borrow money to accomplish something faster than you could otherwise. With technical debt, this is like the initial development boost you get. Once you take out a loan, you need to pay interest on it. With technical debt, this is like how future development will slow down. When you pay down the principal of a loan, the interest gets smaller. This is similar to refactoring, where you spend time improving code to speed up future development.
Like financial debt, technical debt can be a useful tool. Sometimes the short term gains you get outweigh the long term consequences. However, just like taking on too much financial debt can lead to bankruptcy, letting too much technical debt accumulate can cause your product development to slow to a crawl.
If you’ve ever created a new application from scratch, you’ve experienced the wonderful feeling of working on a project without any technical debt. When you’re first building out an application, you can develop new features at incredible speed. There’s no need to worry about the impact on existing users. You can just focus on implementing new features.
However, as an application matures, development speed will inevitably slow down. On a poorly implemented product, development speed slows down quickly. But even on a beautifully implemented one, development speed still slows down over time. As the more code you add to an application, the slower development becomes, I view all code as technical debt.
Adding new assumptions increases debt
As an application matures, it collects a fundamental set of assumptions it makes. When you’re starting a new project, you have no features at all, so there are no assumptions built into your codebase. This makes adding a new feature as simple as implementing the feature itself. However, once your project has its first feature, you’ll forever need to consider the assumptions all the features you’ve developed so far impose on future development.
I’ll give an example of how technical debt from core assumptions accumulates based on my experience creating Doorkeeper, an event platform for communities, which helps organisers of networking events, seminars, and user groups attract participants and manage registration.
We started Doorkeeper to help a local networking event, Mobile Monday Tokyo, make their registration and check in process smoother. For their event, they needed only a single, simple registration flow, where you couldn’t do more than register for an event and cancel your registration.
As we expanded Doorkeeper to meet the needs of other organisers, we discovered many of them had a hard cap on attendance, something that wasn’t a concern for Mobile Monday. To meet the needs of new organisers, we decided to add an option to limit the number of participants.
Having a limited number of participants for an event built on top of our existing assumption that people could register for an event, but we also needed to decide what would happen when someone tries to register for a full event. The simplest solution would have been to block people from registering when it was full, but we thought that gives a disappointing experience to potential participants, and also didn’t give the organiser any feedback about how popular their event was. So we implemented a waitlist feature, where if someone went to register and the event was full, they’d join a waitlist instead. If someone attending the event cancelled their registration, the first person on the waitlist would get their spot.
The next feature we added was prepayment for an event. If our only assumption was “people can register for an event”, this would be straightforward to do, as it builds on top of that assumption. But we also need to look at our other assumptions.
Depending on the event, the organiser may or may not want to allow prepaid participants to cancel. Furthermore, they might want to set up some sort of cancellation policy, where depending on when a participant cancelled, they’d get a partial refund. Because of all the edge cases involved with cancelling prepaid registrations, and that it was something most organisers didn’t want to make too easy, we opted to not allow participants to cancel prepaid registrations themselves. Instead, they’d need to contact the organiser, who could then choose how to handle the situation.
Furthermore, because only a limited number of people could register for an event, we’d need some mechanism to ensure we only let people prepay for a spot they could actually get. To do this, we divided the registration process into two steps: first they entered their registration details, next they went on to payment. If the event filled up between the time they started filling out the initial form and submitted it, they’d join the waitlist.
We also needed to handle the case when they submitted the initial form, but never completed payment. To do this, we added an automatic cancellation of unpaid reservations after a certain amount of time.
We also needed to handle people being moved off the waitlist. For prepaid events, we couldn’t issue their tickets immediately, as instead they needed to come back to the site to complete payment.
As you can see, adding new features became more and more complicated, as we needed to consider the implications off all the assumptions we’d added so far.
Features can have negative value
For a feature to add value to your product, it needs to be useful to users. Features can have a negative value when the technical debt they add to your product outweighs the value they add to it.
An example I’ve seen of a feature that often creates negative value is localisation. At its simplest, localisation is translating your app into multiple languages. There’s more challenges involved than just translation, but lets consider localisation in its simplest form: having a dictionary of language specific strings.
As a first step, this means you can never hard code a string in your application, which adds an extra layer of abstraction. This doesn’t add much overhead, but it is still something your developers always need to keep in mind.
You also need to introduce a translation step into your development process. Assuming your developers aren’t native speakers of all the languages you’re supporting, they can no longer come up with strings themselves, and need to rely on translators. Again, you can work around this, but it still makes all future development a bit slower.
If localisation produces a large amount of value, this overhead isn’t an issue. But I often see apps that do a half-assed job of localising, with the hope people from other countries will start using it. That’s not how it works though. Unless you’re willing to invest in doing a great job of localising and marketing your app in the languages you support, the localisation won’t create value. So you end up with a feature that creates more debt than its worth.
Code isn’t inherently valuable
As developers, it’s tempting to think we’re creating value by writing code. However, the value of software comes from the usefulness of it to users, not the quality of our code. Poorly written code that does a useful task is more valuable than beautifully implemented code that does a useless one.
Because of this, we need to ensure we’re working on valuable features. While traditionally development processes have assumed an omnipotent product owner, who somehow knows the relative value of features, that isn’t really the case.
Hopefully you’re already working with other stakeholders and have a good understanding of why what you’re working on is valuable. But if you don’t, push back. Try to uncover why they believe the feature is valuable and what sort of verification of this has been done.
By making sure the features we implement are valuable, we reduce the chance that the debt they create will be too much of a burden.
Once a feature has been added, it’s there to stay
Part of the reason we need to be so vigilant against features that don’t produce sufficient value is that once a feature has been added, it’s almost always there to stay.
Even when it becomes apparent after the fact that the feature isn’t performing as well as we anticipated, the typical approach is to do nothing. Part of this is the sunk cost fallacy. Often there is the hope that even if the feature isn’t used today, it will be in the future.
But there’s also a legitimate reason for doing nothing. Removing a feature also has a cost, both in the development effort needed to cleanly remove it, and in potentially upsetting customers. So once a feature is added to a product, it is almost always there to stay.
To avoid technical debt, don’t write code
The only sure-fire way of avoiding technical debt is to not write code in the first place. As developers our first instinct is to solve a problem by coding, but that’s not always the best strategy. Often times, we need to push aside that instinct.
A personal example is a job board I run that helps international developers find jobs in Japan. I started by just posting jobs I came across, but eventually heard of enough success stories that I figured companies would be willing to pay for it. As part of this, I wanted to do some basic screening of applications for spam before they went to the company,.
To do this, I started building a screening system using Ruby on Rails. However, after about a day of development, I realised that making a system that worked better than having the candidates send emails was rather complex. I’d need to replicate all the features baked into email: attachments, communication between the candidates and companies, and so on. What’s more, I’d need to ensure the system actually stayed up, monitoring logs and what not.
What I really needed was just a way to moderate the emails before they went to the company. So instead of continuing down the path of implementing something myself, I just used the Groups feature of Google apps to set up a mailing list for each company. While this wasn’t the optimal technical solution, ultimately the value I was creating wasn’t about the technology. Rather, my time was better spent helping companies and candidates find each other.
Work within the constraints of existing assumptions
A technique for reducing technical debt when adding a new feature is to work within the constraints of existing assumptions, rather than adding new ones. I’ll give an example of how we did this in Doorkeeper.
The organiser of an event can send messages to participants. We encouraged organisers to use this to send a reminder to participants. But some organisers never sent a reminder, perhaps because they forgot or were otherwise too busy. On the other hand, other organisers were sending customised reminders, where they provided detailed instructions for the day of the event.
As a new feature, we wanted to make sure participants always got a reminder, and not rely on the organiser to manually send a message.
The most obvious way to add a reminder functionality would be to just automatically send a reminder email to participants the day before the event. This would be easy to implement, but wouldn’t have nicely handled the case where the organiser wanted to send last minute instructions. Those organisers could send a message in addition to the reminder, but then participants would get multiple emails about the same thing, leading to a suboptimal experience. To work around this, we could let the organiser customise this reminder.
The need for being able to customise a reminder led us to realise that there really wasn’t much difference between a reminder and a message. If we automatically scheduled a message to be sent out at a certain time, the organiser could then customise that message, or even disable it entirely.
At the time, we didn’t have the ability to schedule a message to be sent out at a certain time. But letting the organiser schedule messages was also a useful feature more generally. What’s more, keeping the reminders to just be messages allowed us to show all the usual reporting we give organisers, like how many people opened and clicked on the message.
By building upon the existing feature, rather than creating an entirely new standalone one, we were able to come up with a solution that both had better user experience, and created less technical debt.
So to give a quick recap, as adding more code to a product will slow down development, we should view all code as technical debt. To ensure development doesn’t slowly grind to a halt, we need to ensure our code creates more value than debt.
As developers, we should be a champion of our product, defending it from poorly thought out features. I know this can be tough to do, but we can do a lot more good by helping to ensure we create valuable products, rather than just churning out code.