Taking on Good Technical Debt

May 7, 2022

Technical debt is a bad word, but it doesn't have to be the Godfather's offer you can't refuse debt. Organizations do everything to fight the inevitable accumulation of debt (and fail). Yet, most code should and will need to be rewritten. What if I told you technical debt wasn't so bad?

Technical debt is a tradeoff. In a perfect world, your stack would be infinitely modular – the ability to add arbitrary new features without refactoring, switch clouds or technology without pain, and never deal with outdated legacy systems. But in the real world, you must make choices with imperfect information and resource constraints. Nevertheless, taking on good technical debt can create codebases that grow faster and healthier when done correctly.

In finance, companies often look for the optimal capital structure – the mix of debt and equity that a company should use to finance their business while minimizing the weighted cost of capital (WACC).

Like optimal capital structure, the correct type of technical debt often depends on a company's stage, risk profile, and industry. For example, is the company two engineers or two hundred engineers? Is the company an enterprise SaaS company or consumer tech? Does the company differentiate on software or something else?

What kinds of good technical debt can you take on? These choices you make now will inevitably have to be reversed or changed later on but are the most efficient today (including switching costs later on).

Start a project with a monorepo (see U-shaped Utility of Monorepos). When starting a project or company, you don't know where service boundaries will occur. Changing requirements and new knowledge create churn in dependencies – (where should code be, what services should do what).

Choose technology appropriate for your scale. I'm an avid supporter of Kubernetes (see About me), but I would never suggest that a small team adopt it from the start. Even with expert knowledge of Kubernetes, taking it on as a dependency still creates loads of maintenance debt. Instead, choose technology that can naturally evolve into more complex systems (e.g., Fargate on ECS, Cloud Run on Google Cloud).

Use commodity technology when you can. There's a benefit to "engineering with the grain" when you can. You only get a few innovation tokens. Understand your differentiation and commoditize the rest.

A critical piece missing is that I don't believe that it's the most optimized path to outsource every part of your stack (e.g., auth, monitoring, framework, infrastructure), even at the early stages. Attempting to piece together a foundational infrastructure quickly can often turn into a house of cards.

Technical perfection is irrelevance