A lot of my work isn't greenfield. It's inheriting a .NET application that someone built years ago, that still runs the business, and that everyone is now a little afraid of. The code works — until it doesn't — and nobody's quite sure what will break if they touch it.
The hard part is knowing when a legacy app has stopped being "fine, just old" and started actively costing you money and risk. Here are seven signs, drawn from apps I've actually taken over.
1. Every deploy is a held breath
If shipping a small change means a late-night deploy, a manual checklist, and someone on standby to roll back, your release process has become a liability. Healthy apps deploy routinely and boringly. When a one-line fix feels dangerous, the problem isn't the fix — it's that nobody trusts the system to stay up.
2. The person who understood it has left
The classic. The original developer is gone, there's little documentation, and the current team can keep the lights on but can't confidently change anything. Knowledge has quietly walked out the door, and every new requirement now takes far longer than it should because someone has to reverse-engineer intent from code.
3. It's stuck on an unsupported framework
If you're on old .NET Framework versions, an ancient ORM, or libraries that no longer get security patches, you're carrying risk that grows every month. Unsupported runtimes don't just mean missing features — they mean vulnerabilities that will never be fixed, and a hiring problem, because fewer developers want to work on a dead stack.
4. Performance degrades and nobody knows why
Pages that used to load instantly now take seconds. The database is pinned at busy times. The usual reaction is to throw a bigger server at it — which hides the problem for a while and makes it more expensive. Almost always the real cause is a handful of slow queries, missing indexes, or N+1 patterns that a proper audit surfaces quickly.
5. Bugs come back after you fix them
You fix something, it works, and three weeks later it's broken again — or fixing A quietly breaks B. This is the signature of code with no tests and tangled dependencies, where nothing is isolated and every change has invisible side effects. It's exhausting, and it's a reliable sign the codebase needs structural work, not just more patches.
6. You can't add features anymore
When the answer to "can we add X?" is consistently "that'll take months" for things that sound simple, the architecture is fighting you. Business logic tangled into the UI, no separation of concerns, copy-pasted code in a dozen places — all of it means small changes ripple everywhere. The app has become a bottleneck on the business itself.
7. It only runs on one specific, fragile setup
If the app only works on one hand-configured server that nobody dares rebuild, you have a single point of failure with no recovery plan. "We can't move it" and "we can't reproduce the environment" are red flags — the day that machine dies, so does your ability to run the business until someone works out the magic.
How a rescue actually works
The good news: you rarely need to throw everything away. A rescue is a deliberate sequence, not a big-bang rewrite.
Step 1 — Audit before touching anything
The first job is understanding, not changing. I go through the code, the database, the deployment setup and the dependencies, and produce a plain-language picture of the current state: what's risky, what's slow, what's fragile, and what's actually fine. You come out of this knowing where the real problems are instead of guessing.
Step 2 — Stabilise the bleeding
Next, fix the things that cause outages and security exposure — the critical bugs, the worst-performing queries, the most dangerous unsupported dependencies. The goal here is simple: make the app safe to run and safe to touch, so the pressure comes off and there's room to work.
Step 3 — Modernise incrementally
Only then do you improve the structure: introduce tests around the parts you're changing, separate concerns, upgrade frameworks in stages, and set up a repeatable deploy. This happens piece by piece, with the app staying live the whole time. Each step leaves it a little more solid than before.
Rescue or rewrite?
Sometimes a full rewrite genuinely is the right call — but it's a bigger, riskier bet than people assume, because a rewrite has to reproduce years of accumulated business rules that live only in the old code. More often, a staged rescue gets you most of the benefit for a fraction of the risk, because you're never without a working system. The audit is what tells you which path is right; deciding before you understand the app is how rewrites go over budget.
The most expensive legacy app isn't the oldest one — it's the one everyone is afraid to change. Fear is the real cost, and it compounds.
If two or three of these signs sound like your system, it's worth getting a proper look before the next incident forces the decision for you.
Got a legacy .NET app you're nervous about?
I take over ageing ASP.NET and .NET applications — audit, stabilise, then modernise, without downtime. Send me what you're dealing with and I'll tell you honestly whether it's a rescue or a rebuild.
Start a project →