Engineering

The most expensive technical debt is the debt you don't see

Ugly code gets fixed because it's annoying. The debt that actually costs you is invisible - tribal knowledge, dead assumptions, and untested paths.

Rohan Gautam6 min read

Everyone can point at the ugly file. The 900-line controller, the copy-pasted validation logic, the component nobody wants to touch. That debt gets paid down eventually because it hurts every single day. The debt that actually bankrupts a project is the kind nobody can point at, because it lives in someone's head or in an assumption nobody wrote down.

Visible debt gets fixed because it's annoying

A messy function is self-reporting. Every engineer who opens it feels the pain immediately, complains in standup, and eventually someone gets budget to clean it up. I have watched teams schedule entire sprints around a gnarly billing module purely because it kept showing up in every retro. That is debt working as intended, loud enough to force its own repayment.

The problem is that "loud" and "expensive" are not the same thing. A file that's ugly but correct, well-tested, and stable might cost you an afternoon of discomfort a year. Meanwhile a clean, elegant function that quietly assumes something false about your data can cost you a weekend outage and a client's trust.

The debt nobody notices: knowledge, not code

The most expensive debt I've dealt with was never in a diff. It was a payment reconciliation job at a fintech client that ran fine for two years. The code was short, readable, and had decent test coverage. What it didn't have was a comment explaining that it assumed every transaction settled in the same currency it was initiated in.

That assumption was true for two years, until the client launched a multi-currency feature and nobody thought to check the reconciliation job because it "wasn't part of the new feature." It silently miscounted settlements for eleven days before finance caught the mismatch. The fix was four lines. Finding it took three engineers a week, because the person who wrote the original assumption had left the company and nothing in the code hinted that the assumption existed at all.

Warning

A correct-looking function with an unstated assumption is more dangerous than a messy one, because nobody knows to be suspicious of it. Ugly code invites scrutiny; clean code invites trust it hasn't earned.

This kind of debt has a few common shapes:

  • Single points of knowledge. One person understands why the retry logic skips Tuesdays, and it's not written anywhere.
  • Assumptions baked into "done" code. A script that "always" runs after another job, until someone reorders a cron schedule.
  • Tests that check the shape of success, not the substance. A test asserting status === 200 while the actual business logic silently no-ops.
  • Config that was tuned once, for conditions that no longer hold. A timeout set for a database three migrations ago.

None of these show up when you grep for TODOs or run a linter. They only show up when the person who understood them is gone or the conditions they assumed quietly change.

How I actually go looking for it

I stopped trying to find invisible debt by reading code and started finding it by asking questions that code can't answer:

  1. "What would break if this person went on leave for a month?" If the honest answer names a specific human, that's debt, regardless of how clean the code looks.
  2. "What does this job assume about its input that isn't validated anywhere?" I go through cron jobs and background workers specifically, because they run unattended and nobody watches their assumptions decay.
  3. "When was this test last red for a real reason?" A test suite that's been green for a year without ever catching a real bug is either testing nothing meaningful or the code beneath it never changes, and I want to know which.
# a cheap first pass: find scheduled jobs and background workers
# that have gone untouched the longest - they're where assumptions
# tend to fossilize
git log --format='%ad %h %s' --date=short -- 'src/jobs/**' | sort | head -20

Tip

Write the assumption down the moment you notice yourself relying on it, even as a one-line comment. "Assumes settlement currency matches initiation currency" would have saved that client eleven days and a week of debugging.

This is the same instinct behind refactoring legacy code without freezing feature work: you can't fix debt you can't see, so the first job is always making it visible, not clean.

What actually pays this down

Documentation helps, but only the kind that lives next to the code and explains "why," not "what." A README nobody opens is as useless as no README. What works better in practice is forcing knowledge to spread through the team on purpose: pairing on the scary parts, rotating who owns the unattended jobs, and treating "only Sarah understands this" as an incident to fix, not a fact to accept.

It also means changing what you review. Code review catches bad code. It rarely catches a bad assumption, because the assumption isn't written anywhere for a reviewer to question. Ask "what does this depend on that isn't in front of me" as often as you ask "does this work."

Note

This is related to why senior engineers prefer deleting code over adding more of it: fewer moving parts means fewer places for a silent assumption to hide.

Frequently Asked Questions

Isn't messy code still worth fixing?

Yes, but treat it as a comfort improvement, not a risk reduction. Prioritize it below anything where you can't name the person who understands a critical assumption, since that's where an outage is more likely to start.

How do I convince a team to spend time on debt nobody can see?

Frame it in terms of bus factor and incident cost, not code aesthetics. "If this person leaves, we lose the only explanation for X" gets budget approved faster than "this code isn't pretty."

Does more documentation actually solve this?

Only if it's written where engineers already look, next to the code, and explains why a decision was made rather than restating what the code does. Documentation that lives in a separate wiki tends to go stale and gets skipped.

Is this different from regular technical debt?

It's the same debt, just unpriced. Regular technical debt shows up in effort estimates because people can see it. Invisible debt doesn't show up anywhere until the assumption it rests on breaks, which is exactly why it's more expensive on average.

I still keep a messy file open in a tab, half fixed, most weeks. But the thing that actually keeps me up now is asking what my systems assume that nobody's checked in a year. That question has caught more real problems for me than any linter ever has.

If you're building something ambitious and want a partner who sweats these details, get in touch.