
There's a point in any software product's lifespan where your engineers will notice that things are getting harder to change; the code is less familiar; tests are less reliable. It's about here where engineers will raise concerns about the legacy code and technical debt: wanting to divert more and more time and energy away from delivering high value / high impact changes and features, and towards fixing bugs and broken tests.
They're pretty compelling arguments on the face of it: "let us work on fixing this tech debt and then we'll find it easier to deliver all that sweet, sweet value our customers (and sales teams) have been asking for much faster". But what specifically are we arguing is being fixed here? What exactly does this technical debt look like? When does code become "legacy code"? Engineers don't usually have quite as clear an answer for these questions. And, by extension, what the shape of that technical debt is and what constitutes "fixed".
Definitions
First, let's get clear about what Ward Cunningham, who coined the term "technical debt" actually intended by the term.
Technical Debt is the accumulated distance between your understanding of the domain and the understanding that the system reflects
Similarly, when Michael Feathers - who has written extensively about working with legacy code and how to refactor it - was asked to define the characteristics of legacy code, he offered this simple criteria:
Legacy code is code without any tests
Feathers has an additional interpretation for technical debt:
Technical Debt is the refactoring effort needed to add a feature non-invasively
Here, we're talking about both effort and safety, and how that relates to the degrees of connascence (how heavily coupled your dependency tree is) you're having to understand, retain and surgically work within.
How we visualise and regard legacy code or types of debt shapes our attitude around how we think we need to solve the problem. Superficially, technical debt just needs rewritten. Legacy code just needs upgraded. But these problems aren't superficial and they're more than simply old or overly-complicated functions.
Practice vs Principles
The concepts of legacy code and technical debt are closely related but distinct. The commonality between the two however centres heavily around knowledge, learning and understanding. Refactoring code to make it possible to evolve and extend in non-invasive ways is a practical outcome of reducing technical debt, but removing legacy code is as much about building your team's understanding of the domain and creating the conditions to learn about and build knowledge around the domain easily. That means not only ensuring tests are effective and are targeted towards the behaviour of the system under test, but also ensuring comments, documentation and communities of practice reflect that understanding clearly and reliably.
Software engineers love nothing more than writing software. Given the opportunity to refactor something, they'll almost certainly launch into that task immediately, but while that enthusiasm is commendable, the first step should ideally be to discuss the problem space with those communities of practice, explore options and examine the gap between how our organisation wants this part of the system to work, and how it's actually built to work currently. The point at which you want to land is code where tests are easy to write with minimal dependencies and set-up needed, the writer's intent is clear and can be tied to documentation that effectively describes the assumptions and goals for the feature.
A different kind of legacy
The common thread tying legacy code and technical debt together isn't "old" code, deprecated frameworks, or even simply undocumented code. It's about the shared knowledge, confidence and familiarity that can be built around that codebase across the team that helps everyone work with it in a way that continues to enable its evolution and growth. You don't need 10x engineers to solve that problem; you need effective communicators and story-tellers.
Not all legacies are necessarily bad. You can leave a legacy behind after you leave that could take one of any number of forms. The challenge is to leave behind a really positive, supportive one. Something that puts the next person to inhabit your code space in as advantageous a position as possible through descriptive guidance and narratives, relevant and resilient test coverage, and enough shared wisdom within your engineering team circles to help point newcomers in the right direction with sufficient high-level context.