When people say "tech debt," I often hear frustration. The phrase suggests we did something wrong, and now we're paying for it. Sometimes that's true. But more often, "debt" is just a record of a decision made under specific constraints that we didn't come back to. Ward Cunningham put it more constructively:
Although immature code may work fine and be completely acceptable to the customer, excess quantities will make a program unmasterable, leading to extreme specialization of programmers and finally an inflexible product. Shipping first time code is like going into debt. A little debt speeds development so long as it is paid back promptly with a rewrite. Objects make the cost of this transaction tolerable. The danger occurs when the debt is not repaid. Every minute spent on not-quite-right code counts as interest on that debt. Entire engineering organizations can be brought to a stand-still under the debt load of an unconsolidated implementation, object-oriented or otherwise. (Original paper)
If you zoom in, most "debt" started as a tradeoff. Ship without a type system to learn faster (TypeScript overview). Inline a function instead of introducing an abstraction (SOLID intro). Keep a dependency because changing it would block a release. These choices have value. The problem is not choosing them it's forgetting why we chose them, and when we intended to revisit them. Once the context is lost, the workaround hardens into folklore. That's when tiny edits feel risky, upgrades stall, and nobody remembers what happens inside that function everyone calls.
One way out is surprisingly simple: write down decisions. Not essays just small decision records. What did we consider? What did we pick? Under which assumptions? What would make us change our mind? If you've seen Architecture Decision Records (ADRs), this is similar but it can be even lighter (adr.github.io). The point is continuity. When teams change and code evolves, the intent stays visible. You don't need a "debt" label to justify work; you need a reason someone else can understand.
Another helpful shift is to make the "interest" observable. If we say debt slows us down, how? Longer cycle times? More rollbacks? Fear around touching certain files? Pick a few signals and watch them. Add tests around places where breakage would hurt (testing primer), measure build and deploy times (DORA metrics), log which modules trigger incidents (MTTR explained). This is debugging the development process. If you can see where the friction is, you can iterate on it without getting stuck in arguments about purity.
This also changes how we plan the work. We don't have to put "cleanup" on a pedestal. Instead, we can shape small steps and link them to outcomes. If a fix fits in a sprint, write a story with clear acceptance criteria and ship it. If it touches multiple modules or requires sequencing, treat it as an epic. If it crosses teams or unblocks strategy, reliability, compliance, upgrades call it an initiative. The labels aren't the point; the clarity is. Connect these to decision records so future you understands why the work existed.
Stakeholder conversations get easier when you talk about effects, not virtues. "We expect lead time to drop by 20% after extracting the slow path." "Upgrading X unblocks Y." "Adding tests around Z reduces incident recovery time." These are hypotheses; they don't have to be perfect. But they're testable, and that's enough to start. Guilt doesn't schedule work. Outcomes do.
Do we ever need a rewrite? Sometimes. But rewrites are just one tool, and they have sharp edges. A safer pattern is incremental change: add tests on critical paths, extract seams (Strangler Fig pattern), delete dead code, isolate hotspots. If you keep the changes small and frequent, they compound. When larger moves are necessary, design them like experiments with guardrails: feature flags (LaunchDarkly primer), strangler patterns, staged rollouts, a clear rollback. Make the path reversible. That alone reduces the fear.
Legacy systems don't have to feel like haunted houses. They're evidence that your software survived reality. If you keep decisions visible and the feedback loops healthy, you can evolve without drama. Ship when it matters. Document why the tradeoff made sense. Revisit when the signals say it's time. Pay the interest with steady work. And when a big move is due, make it boring and safe rather than heroic and brittle.