Meditations on technical bankruptcy

Paying due on a system in default

A few years ago, I watched a senior developer spend an entire sprint trying to add what should have been a simple feature: displaying a user’s timezone on their profile page. The complexity wasn’t in the feature itself. The problem was that our codebase had become a minefield of implicit assumptions, where touching any single component risked detonating a cascade of failures across seemingly unrelated systems.

That was my first encounter with what I now think of as technical bankruptcy: the point where a system’s accumulated dysfunction makes forward progress effectively impossible without wholesale restructuring. Not technical debt, which implies you can still make payments. Technical bankruptcy is when you’ve defaulted, and the only question is whether to negotiate terms with your creditors or burn it all down and start over.

The distinction between technical debt and technical bankruptcy matters more than most engineers realize. Technical debt is a useful financial metaphor because it captures something true about tradeoffs: sometimes you should borrow against the future to ship faster today1. The problem is that the metaphor breaks down at exactly the point where it becomes most important.

Real financial debt has clear terms. You know your interest rate, your payment schedule, the total amount owed. Technical debt is nothing like this. It’s more like a payday loan from a company that keeps changing its interest rates based on inscrutable criteria, compounds daily, and will break your kneecaps if you miss a payment, except the payment schedule is also secret.

You might think you’re accumulating manageable debt by skipping a few tests here, taking some shortcuts there, putting off that refactor for next quarter. What you’re actually doing is buying lottery tickets, where each ticket has an unknown probability of catastrophic failure. Most tickets are duds. Some blow up your entire system.

The transition from debt to bankruptcy isn’t gradual. It’s a phase change. One day you’re making steady progress while accruing some debt. The next day, nothing works anymore, and you can’t figure out why, and fixing one thing breaks three others, and your estimates are off by orders of magnitude because the system has become fundamentally unpredictable.

The topology of failure

Here’s what technical bankruptcy actually looks like on the ground:

You have a system that works. It’s serving users, making money, fulfilling its purpose. But the internal structure has become so convoluted that any change, no matter how small, requires touching dozens of files, understanding hundreds of implicit dependencies, and hoping that nothing breaks in the vast surface area of potential interactions.

Your test suite takes six hours to run, assuming it doesn’t flake out halfway through. Half the tests are testing the wrong things. The other half would catch bugs, but they’re too slow to run during development, so people push changes without running them. Which means the tests often fail in CI, which means people learn to ignore failed tests, which means the tests stop being useful, which means people stop writing new tests, which means…

Documentation, where it exists, describes a system that no longer exists. “This service handles authentication,” reads the README, except authentication was split across three services two years ago, and nobody updated the README, and anyway the person who wrote it left eighteen months ago, and the new authentication system uses a completely different architecture that nobody documented at all because they were too busy fixing the fires caused by migrating from the old system.

Your build process requires specific versions of twelve different tools, two of which are deprecated, one of which only compiles on Ubuntu 18.04, and all of which must be invoked in exactly the right sequence with exactly the right environment variables or the build silently produces broken artifacts. Getting a new developer’s environment working takes three days of pair programming with someone who knows all the tricks.

New features take progressively longer to ship. What took a week two years ago now takes two months. Not because your engineers got slower. Because the system got heavier. Every change requires archaeological expeditions through layers of cruft, careful negotiation with Byzantine systems that cannot be modified without breaking, and elaborate workarounds for fundamental architectural decisions that made sense five years ago but are now weight around your neck.

This is technical bankruptcy. When the cost of moving forward has become so high that you’re effectively paralyzed.

Why good teams go bankrupt

The natural assumption is that technical bankruptcy happens because of bad engineering. Lazy developers, incompetent management, lack of investment in quality. And sure, these don’t help. But I’ve seen technically bankrupt systems built by extremely competent engineers making reasonable decisions at every step.

The problem is that reasonable local decisions can produce unreasonable global outcomes. This is not a new insight2, but it’s worth dwelling on in this context because the mechanisms of technical bankruptcy are particularly insidious.

Consider: You’re building a feature. You need to fetch some data. There’s an existing service that provides similar data, but not quite what you need. You have three options:

  1. Modify the existing service to support your use case
  2. Create a new service that does exactly what you need
  3. Work around the existing service’s limitations in your code

Option 1 means understanding someone else’s code, which takes time. It means coordinating with the team that owns that service. It means your feature is blocked on their sprint priorities. It means your changes might break their existing use cases.

Option 2 means creating more infrastructure. More services to maintain, more complexity in the overall system. It means duplication, because your new service will inevitably share some code with the existing one.

Option 3 is quick. You can do it right now. It doesn’t require coordination. It doesn’t create new infrastructure. It’s just a few lines of code that massage the data into the shape you need.

Option 3 is almost always the right choice locally. And it’s almost always the wrong choice globally.

Because when everyone makes choice 3, you end up with a system where every component is working around every other component’s limitations. The actual behavior of any given service bears little resemblance to what it was designed to do. The dependencies between services are not the clean architectural lines on your service map, but a tangled web of implicit assumptions and brittle workarounds3.

This is how good teams go bankrupt. Not through any single bad decision, but through the accumulation of individually reasonable choices that interact in unreasonable ways.

Bankruptcy as honest accounting

Here’s an uncomfortable thought: maybe technical bankruptcy isn’t a failure state. Maybe it’s just honesty.

Most systems are carrying way more technical debt than anyone wants to admit. We pretend we’re in decent shape because acknowledging the full scope of the problem feels impossible. We estimate features as if we’re working in a healthy codebase, then scramble to explain why everything takes three times longer than expected.

Technical bankruptcy is when you stop pretending. When you acknowledge that the emperor has no clothes, the codebase is held together with duct tape and prayers, and any serious new development is going to be a slog through molasses.

There’s something almost clarifying about this admission. Once you’ve declared bankruptcy, certain options open up that weren’t available when you were maintaining the fiction of solvency.

You can tell stakeholders the truth: this will take six months, not six weeks, because the system is fundamentally broken. You can prioritize the restructuring that you’ve been putting off because it seemed less urgent than features. You can make the case for a ground-up rewrite, which is usually a terrible idea but sometimes is actually the least bad option.

Of course, this only works if your organization can handle honesty. Many can’t. Admitting technical bankruptcy can be career limiting, even if you’re not the one who caused it. The messenger gets shot. The company decides the engineering team is incompetent. Management panics and makes everything worse.

But if you’re in an organization that can handle the truth, declaring bankruptcy might be the first step toward actual recovery.

The rewrite question

Every discussion of technical bankruptcy eventually arrives at the rewrite question: should you throw everything away and start over?

The standard answer is no. Joel Spolsky’s famous essay on why you should never rewrite from scratch has attained almost scriptural status in software engineering. The argument is simple: the old codebase, however ugly, represents thousands of hours of work, hundreds of bugs found and fixed, countless edge cases handled. Throwing it away means rediscovering all those bugs, re-handling all those edge cases, and probably taking years to reach feature parity with what you already have.

This is usually correct. But not always.

The logic of the anti-rewrite position assumes that the primary value in your old codebase is the knowledge embedded in all those bug fixes and edge cases. And if that’s true, then yes, throwing it away is madness.

But what if the primary thing embedded in your old codebase is dysfunction? What if those thousands of hours of work have produced a system that makes simple things hard and hard things impossible? What if the edge cases are edge cases precisely because the original architecture was wrong?

I think there’s a threshold, difficult to identify precisely, where the balance tips. Below the threshold, the knowledge in your old codebase exceeds its dysfunction. Above the threshold, the dysfunction exceeds the knowledge. Rewriting below the threshold is foolish. Rewriting above the threshold might be the only way forward.

The hard part is knowing which side of the threshold you’re on. Everyone thinks their codebase is uniquely terrible. Everyone thinks a rewrite would solve their problems. And most of the time they’re wrong.

Here’s my heuristic: if you can estimate how long it would take to rewrite your core system from scratch, you’re probably below the threshold. If you literally cannot estimate it because the requirements have become too complex and intertwined to even specify, you might be above it4.

Living with bankruptcy

Let’s say you’ve declared technical bankruptcy, you’ve decided against a full rewrite (because you usually should), and you’re stuck with the system you have. What then?

The first thing to accept is that you’re playing a different game now. You’re no longer optimizing for velocity or growth. You’re optimizing for survivability. The question isn’t “how do we ship faster” but “how do we avoid total collapse.”

This means triage. Brutal, honest triage. Some systems need to be kept alive no matter what. Others you let die. The systems that are load-bearing for your actual business: those get resources. The systems that seemed like a good idea three years ago but nobody uses anymore: those get unplugged.

You need circuit breakers everywhere. When something fails, it needs to fail gracefully, without taking down the rest of the system. This is expensive to build properly, but it’s the only way to survive in a system where failures are common and cascading failures are lethal.

You need observability. Not the nice-to-have observability of healthy systems, but the desperate we-literally-cannot-function-without-this observability of bankrupt ones. You need to know exactly what’s happening, exactly what’s failing, exactly what’s slow. Because in a healthy system, you can wait until tomorrow to debug something. In a bankrupt system, waiting until tomorrow means the whole thing might be down.

Most importantly, you need to stop digging. Every new feature, every new integration, every new “quick workaround” makes the bankruptcy worse. This is the hardest discipline because it requires saying no to things that seem important, that your stakeholders want, that your competitors are doing.

But if you’re technically bankrupt and you keep adding complexity, you’re just postponing the collapse while making it worse when it comes.

The long recovery

Here’s what nobody tells you about recovering from technical bankruptcy: it takes years. Not months. Years.

You can’t fix a deeply broken system quickly because the problems are systemic. They’re in the architecture, the patterns, the assumptions, the organizational knowledge. Fixing them requires changing all of these things, and change at that level is necessarily slow.

The only way I’ve seen this work is through a strategy of strategic replacement. You don’t rewrite everything. You identify the components that are causing the most pain and rebuild them, one at a time, with clean boundaries and clear contracts. You do this while keeping the old system running, which means you’re maintaining two systems in parallel for a while, which is expensive and messy.

Each replacement makes the next one easier. Because each time you build something new, you have an opportunity to do it right. Clear interfaces. Good tests. Proper documentation. And as you accumulate these well-built components, they become the foundation for future work.

This is a Ship of Theseus approach to system recovery5. You replace the system piece by piece until eventually you’ve replaced most of it, and at no point did the system stop working. It’s slow. It’s expensive. It requires discipline and patience and a willingness to make short-term sacrifices for long-term health.

But it works. I’ve seen it work. It takes three years, maybe five, and at the end you don’t have a perfect system, but you have a functioning one, and that’s enough.

Recognizing insolvency early

The time to think about technical bankruptcy is before you’re bankrupt. Which means recognizing the warning signs while you still have options.

Here are mine:

When your estimates are consistently off by more than 2x, something is wrong. Individual estimates can be wrong. But if everything takes twice as long as you think it should, the problem isn’t estimation. The problem is the system.

When simple changes require touching more than five files, your abstractions are wrong. Good abstractions localize change. If every feature requires changes scattered across the codebase, your boundaries are in the wrong places.

When you’re afraid to change code, you have a trust problem. Maybe the tests are inadequate. Maybe the code is too complex to reason about. Maybe both. But if the dominant emotion when approaching a change is fear, you’re heading toward bankruptcy.

When “we’ll fix that later” becomes a meme on your team, you’re not fixing things later. You’re accumulating debt faster than you’re paying it down. The bankruptcy might be years away, but you’re on the path.

When the institutional knowledge required to maintain the system exceeds what any individual can hold, you’re becoming dependent on heroics. And heroes quit, burn out, or get hit by buses. A system that requires heroes to maintain is already in danger.

The bankruptcy mindset

I think the most important thing I’ve learned about technical bankruptcy is that it’s as much a mindset as a technical state. You can have a fairly healthy codebase and still be thinking like a bankrupt team: always in crisis mode, always fighting fires, always choosing short-term survival over long-term health.

And conversely, you can have a genuinely bankrupt system but approach it with a healthy mindset: clear about the problems, realistic about the timeline, disciplined about not making things worse.

The bankrupt mindset is characterized by learned helplessness. Nothing can be fixed, everything is terrible, we’re just trying to survive until tomorrow. This mindset makes bankruptcy a self-fulfilling prophecy. If you believe the system is unfixable, you won’t make the investments that could fix it. You’ll just keep applying band-aids and wondering why nothing gets better.

The healthy mindset, even in the face of bankruptcy, maintains agency. Yes, the system is broken. Yes, it will take years to fix. But we can fix it, and here’s how, and here’s what we’re doing this quarter that moves us toward that goal.

This distinction matters more than almost anything technical. I’ve seen teams with objectively terrible codebases make steady progress through sheer determination and good process. And I’ve seen teams with decent codebases spiral into dysfunction because they’d given up on ever making things better.

Final thoughts on debt and default

Technical bankruptcy is not a moral failing. It’s not even necessarily a mistake. Sometimes it’s just what happens when you prioritize other things, when you move fast and accept the consequences, when you make the tradeoffs that let you survive until tomorrow.

The mistake is not recognizing it. The mistake is pretending you’re solvent when you’re not. The mistake is continuing to accrue debt when you should be in recovery mode.

If your system is technically bankrupt, you need to acknowledge it. Not to your users, necessarily, or even to your stakeholders. But at least to yourself and your team. Because only then can you start making the kinds of decisions that might actually help.

And if your system isn’t bankrupt yet, but you can see it on the horizon: now is the time. Now is when you can still choose prevention over cure. When you can still refactor instead of rewrite, pay down debt instead of declare bankruptcy.

The house of cards is always asking for its mortgage payment. The question is whether you’re paying it down or letting it accumulate until the whole structure collapses.

Choose wisely.


  1. The original technical debt metaphor comes from Ward Cunningham in a 1992 experience report. He was very explicit that the metaphor only works if you actually intend to pay the debt back, and that taking on debt you can’t repay is just bad practice. We’ve largely forgotten this caveat. ↩︎

  2. This is related to what Garrett Hardin called “the tragedy of the commons” and what Scott Alexander has written about as coordination problems. The specific application to technical systems is less explored but follows the same logic. ↩︎

  3. I’m reminded of the Chesterton’s Fence principle, except in reverse. In a technically bankrupt system, every fence was built to work around some other fence, and nobody can remember why any of the fences are there, but removing any one of them causes three others to fall over. ↩︎

  4. This heuristic is related to the Cynefin framework for decision-making. A system you can estimate is complicated. A system you literally cannot estimate has moved into the complex or chaotic domains, where different approaches are needed. ↩︎

  5. The Ship of Theseus is an ancient thought experiment about identity and change. In the context of software, it’s actually a pretty good model for how systems evolve: gradual replacement while maintaining continuity of function and identity. ↩︎

‹ All posts