← Back to notes
Engineering

The Legacy Code Audit Checklist: What to Assess Before You Rewrite Anything

June 29, 20269 min read

Every rewrite-vs-refactor conversation starts the same way: someone in the room is certain the codebase is unsalvageable, someone else is certain it just needs a cleanup sprint. Both are usually guessing. Neither has actually opened the system up to look at what is load-bearing, what is dead weight, and what nobody currently understands. That is the gap an audit closes.

An audit is not a scoring exercise that produces a number out of ten. It is a structured pass through the system that answers one question, repeatedly, in every part of the codebase: if we touch this, what breaks, and would we know? Do that consistently and the refactor-or-rebuild decision stops being a judgment call and becomes a conclusion the evidence supports. Skip it, and whatever you decide is a bet dressed up as a strategy.

Below is the checklist we actually run. It is what we call Discovery — the first of the four phases we work in, laid out in full on our services page — organized here into seven areas. None are optional: a system can look fine on one axis and be a liability on another, and the axis you skip is usually the one that costs the most six months into the project.

1. Architecture and coupling

The first question is not “is this a monolith” — most legacy systems are, and that alone tells you little. The real question is how tangled the dependencies are inside it. A large, well-layered monolith with clean module boundaries is a very different asset than a smaller one where every class reaches into every other class.

  • Map the actual dependency graph between modules, not the one in the architecture diagram from three years ago. Tools that generate call graphs and import graphs are cheap; trusting stale documentation is not.
  • Count the cyclic dependencies. Circular references between modules are the clearest single signal that “just extract this piece” will not be simple.
  • Identify god objects and god services — the handful of classes or endpoints everything else depends on. These are your blast-radius centers; any change strategy has to route around or through them deliberately.
  • Separate genuine domain complexity from accidental complexity. Some of what looks tangled is the business itself being complicated — that has to survive a rewrite. Complexity caused by a decade of shortcuts does not.

The output is not a diagram for its own sake. It is a ranked list of which subsystems can be touched in isolation and which cannot be touched without touching everything else.

2. Data model and data quality

Code is easier to reason about than data, which is exactly why data debt survives longest and does the most damage. A schema that has absorbed a decade of “just add a column” decisions is rarely documented anywhere except in the columns themselves.

  • Check for orphaned data: rows with no valid foreign key target, foreign keys never enforced at the database level, tables nothing in the current codebase writes to anymore.
  • Look for business logic hiding in triggers, stored procedures, and database functions. This is the single most common source of surprise in a rewrite — validation rules or state transitions that exist only in the database layer and were never reflected in application code or documentation.
  • Audit schema debt directly: nullable columns that are never actually null in practice, columns whose meaning has drifted from their name, enum-like fields implemented as free text.
  • Trace a handful of critical fields end to end — where they are written, where they are read, and whether every reader agrees on what the value means. Disagreement here is where rewrites quietly introduce correctness bugs.

If nobody can produce an accurate entity-relationship diagram for the live database without generating one from the schema itself, that is a finding on its own — the data model’s real behavior is undocumented, and any migration plan needs to budget time to reverse-engineer it rather than assume it.

3. Test coverage and change safety

Coverage percentage is a weak proxy for the question that actually matters: can this system be changed safely today? A codebase with 40% coverage concentrated on the right seams can be safer to modify than one with 80% coverage made up of shallow tests that assert nothing meaningful.

  • Distinguish tests that verify behavior from tests that verify implementation. The latter break on every refactor regardless of whether behavior changed, and will actively slow down modernization work rather than protect it.
  • Check whether critical business paths — the ones that generate revenue or carry compliance risk — have any coverage at all. It is common to find exhaustive tests on peripheral utilities and none on the workflow that actually matters.
  • Run the existing suite and note how long it takes and how often it is flaky. A suite nobody trusts is functionally equivalent to no suite — it gets skipped under deadline pressure, exactly when it is needed most.
  • Where coverage is thin on critical paths, characterization tests — pinning down current behavior, correct or not — are worth writing before any rewrite starts. They turn “did we preserve behavior” from a debate into a test run.

4. The operational picture

How a system behaves in production tells you things static analysis of the code never will. This is where you find out whether the system is actually stable or just quiet.

  • Pull deploy frequency and deploy failure rate over the last six to twelve months. A system that gets deployed weekly with a low rollback rate is being actively maintained by people who understand it. A system deployed quarterly, gingerly, by one specific person, is telling you something different.
  • Review incident history: not just count, but root cause. Repeated incidents tracing back to the same subsystem identify exactly where the architectural risk concentrates.
  • Look at on-call burden for this system specifically. A system that pages someone every week has a real, ongoing cost that rarely shows up in a project budget but shows up in team attrition.
  • Check monitoring and alerting coverage. If an outage would first be noticed by a customer complaint rather than an alert, that is an operational gap independent of code quality, and it needs fixing regardless of what you decide to do with the code itself.

5. Bus factor

This is the item organizations most often skip, because it is uncomfortable rather than technical. It is also frequently the deciding factor in refactor-versus-rebuild — a system nobody understands cannot be safely refactored incrementally, because every change is a guess.

  • For each major subsystem, identify who can currently explain how it works, not just who has touched the code. Those are different lists, and the second one is usually shorter.
  • Ask what happens if that person leaves next month. If the honest answer involves “we would have to figure it out from the code,” you have already found your highest-risk subsystem, independent of how well-engineered it actually is.
  • Check whether institutional knowledge is written down anywhere durable, or whether it lives entirely in one or two people’s heads.
  • Weigh this against the architecture findings. A tangled subsystem three people understand well is a manageable refactor. A clean subsystem only one person understands is a landmine — you will not know it is fragile until that person is unavailable.

6. Third-party and vendor dependency risk

Legacy systems accumulate dependencies the way old houses accumulate wiring: added in layers, rarely removed, and eventually nobody is sure what is still live.

  • Inventory every direct dependency and its support status. Flag anything end-of-life, anything on a version no longer receiving security patches, and anything maintained by a single volunteer with no recent commits.
  • Separate dependencies that are swappable from ones the system is architecturally welded to. A logging library is swappable. A framework the entire request lifecycle is built around is not — replacing it is closer to a rewrite than an upgrade.
  • Identify vendor and integration dependencies outside the codebase itself: unsupported APIs, deprecated webhooks, integrations with systems themselves scheduled for retirement. These constrain your options as much as anything in the code does.

7. Security posture

Security debt in legacy systems is rarely one dramatic vulnerability. It is usually a long accumulation of smaller gaps that compound: authentication logic predating current standards, secrets checked in where they should not be, input validation adequate for a decade-old threat model.

  • Run dependency vulnerability scanning against the full inventory from the previous section — known CVEs in outdated libraries are the fastest finding to produce and often the most urgent to act on regardless of the broader modernization timeline.
  • Review authentication and authorization logic directly rather than trusting that it “has always worked.” Access control bugs in old systems tend to be additive — a special case bolted on for one customer years ago that nobody has revisited since.
  • Check secrets management: credentials in config files, environment variables committed to version control history, API keys that have never been rotated.

What the audit actually buys you

Run all seven and you end up with something more useful than a recommendation: a map. Some subsystems come out looking merely old — sound architecture, adequate tests, reasonable operational history, just due for a dependency upgrade. Leave those alone. Others come out tangled but well-understood, with a team that can name every landmine — refactor candidates, tackled incrementally behind characterization tests. And a few come out both fragile and orphaned: nobody understands them, nothing protects them, and the data underneath is inconsistent with what the code assumes. Those are your rebuild candidates, and now you can say why.

That is the value of doing this work up front. Not certainty — audits do not eliminate risk, they locate it — but a decision you can defend to an engineering team, a board, or your future self eighteen months in when someone asks why this system got rebuilt while the one next to it just got refactored.

This is also why Discovery is treated as its own phase rather than a preamble to the real work. The refactor-rebuild-or-leave-alone call this audit produces is the direct input to Architecture — the system design and the plan you can hold a team to. Everything after that, from the short cycles of Build to running the result in production under Operate, inherits whatever Discovery got right or missed. Skip straight to Architecture without it and you are not saving time — you are deferring the audit to the middle of the project, where it is far more expensive to run. If you want a second set of eyes on where your own systems land on this map, that is the kind of initial assessment we run before proposing a scope.

The rewrite decision is expensive to get wrong in either direction — rebuilding something that only needed a cleanup, or patching something that needed to be replaced two years ago. The audit is what makes the decision cheap to get right.

Have a legacy system that's holding you back?

Tell us where it hurts. We'll tell you honestly whether it's worth fixing, and how.