Technical debt — it doesn't have to be this way
A common opinion is that technical debt in a startup is inevitable, a function of moving fast, minimum viable products (MVPs), prototypes, agility and the pressure of having to reach product/market fit as fast as possible.
The excuse of building an MVP to validate an idea allows one to cut corners to test features at the expense of code quality. Some even reduce thinking time at this stage and sit down as soon as it’s agreed that the feature should be tested, hammering out out code that I like to call: Just enough code. It’s functionally complete, it will pass all unit tests and it works now. With the only goal being to get the feature live as soon as possible, what is not considered is the prospect of any future iterations.
I’ve seen it many times where the pressure to continually prove product/market fit ensures that these initial MVP/prototypes become the foundations of huge, long-living codebases. Good intentions early on (“we can rewrite this once we’ve tested it”) are forgotten in the rush and excitement to build and test the next iteration, not to mention the pressure from stakeholders to show progress by releasing new features and not “wasting time” on refactoring.
I posit that this is not the way you should do things even in those early manic days.
The downsides of having technical debt in your codebase are multiple.
Cutting corners in code can help you release a feature quickly, however this is only true the first few times. Once you have to go back into this code and figure out what’s going on and spend time reworking it then you’re adding time to future iterations. Over time you end up with spaghetti code, coupling together parts of a system that with some thought could have been kept modular.
“Spaghetti code is a pejorative term for sourcecode that has a complex and tangled control structure, especially one using many GOTO statements, exceptions, threads, or other “unstructured” branching constructs”—Wikipedia
This increases the complexity of your codebase exponentially until you get to a point where some parts of the codebase are no-go areas. Nobody really understands how they work or the unwritten layers of logic that have accumulated over time.
This introduces an extra step in the development process where you have to first consider the quality of the code you’re working on. Different parts of the codebase have different quality states and you now have to determine the solidity of these foundations before thinking about how you’re going to build your next feature. These states won’t be obvious—there’s no incentive to document such areas if they’re allowed — so you have to work it out for yourself. Not only is this time-consuming and error-prone, but this extra friction means engineers are less likely to build new things. Head space is also taken up by the refactoring efforts required and therefore less thought is given to the actual problem. More code changes mean more opportunities to introduce bugs. Estimates become more difficult as you have to include refactoring work.
Even with a mindset of minimising technical debt, cutting corners may be necessary. However this occurs much less frequently — for example, testing a feature that would require significant engineering work for 1M users can be built in an order of magnitude less time if you only build it for 10,000 users, enough to test its value—so all shortcuts are clearly documented and there’s no requirement to manually evaluate the state of the supporting code before you begin.
Minimising technical debt does not mean zero technical debt. However, it does mean zero deliberate technical debt. The distinction is that you build to the best of your ability with your current knowledge of the problem domain. Later learnings in this domain change your perception of what is possible and a better way may become apparent. Therefore, you should learn as much as you can before writing any code. Discuss the problem with peers, hold whiteboard sessions explaining the problem and proposed solutions and debate these proposals in pull requests before writing anything. This is particularly important when data structures and models are concerned as changing these later is much more work as you have to move data around — potentially introducing downtime in your product — as well as changing the underlying code.
While you can use nearly all of the above as an argument for introducing this mindset into your organisation, the main reason I advocate minimising technical debt is probably something a non-technical manager will never understand: working on a clean codebase is simply more fun.