Featured image of post Effectiveness anti-patterns — part 2: technical debt denial

Effectiveness anti-patterns — part 2: technical debt denial

The second part of a series on engineering effectiveness anti-patterns, focused on what happens when we keep postponing system health in favor of short-term feature delivery.

After writing the first part of this series, I kept thinking about another anti-pattern from The Effective Software Engineer by Addy Osmani that feels just as common, and maybe more expensive over time: ignoring system health while continuing to push feature work.

This is again inspired by Addy’s framing, but I want to keep this post personal, just like the first one. Not as a summary of the book, and not as a strict rewrite of his structure. More as a reflection on something I have seen in teams — and something I have also contributed to at times.

Technical debt denial rarely feels irresponsible in the moment

That’s usually how it starts. Nothing dramatic.

Almost nobody says, “Let’s make the system worse on purpose.” The decision usually sounds much more reasonable. We tell ourselves that this sprint is not the right time. That the refactor can wait one more release. That shipping the feature now matters more, and we will clean things up later.

Sometimes that tradeoff is valid.

I do not think every maintenance issue needs to be fixed immediately. But I have seen how easily temporary compromises become the default operating mode.

Debt does not slow you down immediately. That is why it is dangerous.
You only feel it later.

The cost shows up later — in how fast you can move and how much you trust what you ship

What I like about Addy Osmani’s approach is that he frames technical debt as an effectiveness problem.

When system health is ignored for too long, the impact shows up everywhere:

  • simple changes take longer because people work around fragile code
  • debugging slows down as confidence in the system drops
  • delivery becomes less predictable due to hidden risk
  • engineers avoid parts of the codebase they know will be painful

I have seen this pattern in real work. A part of the system is “messy but manageable.” Then a few more shortcuts stack up, a few more exceptions get added, and suddenly even small changes feel heavier than they should.

At that point, the team is still delivering — just with more hesitation.

One of the traps is that debt hides behind productivity

This is the tricky part.

A team can look productive while system health is quietly deteriorating underneath. Features ship. Tickets close. From the outside, everything looks fine.

Inside the team, the signals show up earlier:

  • certain areas are always described as risky
  • estimates keep growing for work that sounds straightforward
  • bugs reappear around the same components
  • engineers say “please do not touch that part unless you have to”

You open a file and you already know what kind of experience it will be.
Maybe it’s a 500-line function with flags stacked on flags.

Too many conditionals. Unclear ownership. Fragile tests, or no tests at all.

The work becomes mentally expensive before it becomes technically difficult.

I do not think the answer is “stop building features”

What I’ve landed on so far is this: it’s not really a feature vs. system health problem.

It is that treating technical debt as optional cleanup does not work.

Because everything else is never done.

What I have seen work better is making maintenance part of normal delivery:

  • reserving explicit capacity for technical improvements instead of hoping spare time appears
  • making debt visible in plain language, including the delivery cost it creates
  • improving small parts of the system while working on nearby features
  • being honest in planning when a fast option creates future drag

Explaining debt in terms of impact helps. Saying “this area now makes similar changes take twice as long” lands very differently than “the codebase feels messy.”

Personal note: I have learned to pay attention to repeated friction

One thing I try to notice now is repeated pain.

If the same part of the system keeps slowing people down, keeps producing avoidable bugs, or keeps requiring extra care every time we touch it, that is usually not random.

It is a signal.

Earlier in my career, I underestimated that. I focused on individual tasks: ship this, then move on. Over time, I have come to see that effectiveness depends heavily on the condition of the environment we are building in.

If the foundation is hard to work with, even strong engineers start looking slower than they really are.

My current view on handling technical debt better

  • treat recurring friction as real data: repetition usually means something structural underneath

  • improve things while doing nearby work: small improvements compound over time

  • make the cost easier to explain: slower delivery, harder onboarding, more regression risk

  • avoid normalizing low-confidence systems: if everyone avoids the same code, it is worth addressing

Closing thought

This anti-pattern resonates with me because it is rarely about laziness or bad intent. More often it comes from good people trying to stay useful under pressure.

But if nobody protects system health, the system eventually starts fighting back.

That is what I appreciate in Addy Osmani’s work here: connecting technical debt to effectiveness, not just code quality.

That’s the part that stuck with me.

Taking care of the system is not a detour from progress.

It is what keeps progress possible.

Posts in this series:

Further reading:

Built with Hugo
Theme Stack designed by Jimmy