How to Audit a Legacy Codebase: A Step by Step Guide
Every engineer inherits a legacy codebase eventually. A new job, a team handover, an acquisition, or simply a product you wrote three years ago that no longer feels familiar. The instinct is usually to open the files and start reading, but that is the slowest way to build understanding.
A structured audit is faster, safer, and produces an artefact you can share with the rest of the team. This guide walks through how to analyse a legacy codebase step by step, using the same approach that platform engineers apply to complex systems in production.
If you are looking for broader guidance on how to make changes to legacy code once you understand it, our article on working with legacy code covers the refactoring side. This piece is about the diagnostic work that comes first.
Why Legacy Code Analysis Matters
The hardest part of working with an unfamiliar codebase is not writing new code. It is knowing where it is safe to write new code. Without an audit, every change carries hidden risk: a shared utility you did not realise was called by 40 other modules, a database migration that has never been tested, a third party dependency with a known vulnerability quietly sitting in production.
A proper analysis gives you:
- A map of what exists and how it fits together
- A ranked list of risks, from security issues to fragile components
- Evidence you can use to justify time for improvements
- A starting point that is unlikely to cause regressions
Skipping this step is how teams end up rewriting the same module twice, or shipping a refactor that breaks a feature nobody remembered was there.
Before You Start
Set expectations with your team or manager. An audit is not a rewrite and it is not a full documentation pass. It is a time boxed exercise that produces a clear report.
Agree on three things up front:
- Scope. Which repositories, services, or modules are in scope? A focused audit on a single service is far more useful than a vague attempt at the whole estate.
- Time box. One to two weeks for a medium service is realistic. Large monoliths may need three to four weeks and more than one engineer.
- Output. A short written report with findings, risks, and recommendations. No one reads a 60 page document.
With those agreed, you can start.
Step 1: Get the Code Running Locally
You cannot audit what you cannot run. The first task is to stand up the application on your own machine with a realistic dataset.
Note everything that goes wrong. Missing environment variables, outdated Docker images, database seeds that no longer work, and undocumented setup steps are all findings. If it takes you a day to get it running, it takes every new joiner a day too. That is a cost worth surfacing.
As you go, capture the setup steps in a draft document. By the end of the audit you will have a working local environment guide, which is often the single most valuable artefact for the team.
For more on handling environment configuration in legacy projects, see our guide to environment variables done right.
Step 2: Run the Basics
Before reading any source code, run a set of standard tools. These take minutes and give you a baseline picture of the codebase.
Count the Code
Use cloc ↗ or tokei ↗ to count lines of code by language.
cloc .
This tells you the shape of the codebase at a glance. A 40,000 line Python service with a 5,000 line JavaScript frontend is a very different system from a 200,000 line Java monolith with three generations of templating.
Check Test Coverage
Run the existing test suite and record the coverage number, even if it is low or zero. Make a note of which parts of the code are covered and which are not. Coverage is an imperfect metric, but the shape of the coverage map tells you which modules are well tested and which are effectively untested.
Audit Dependencies
Run the dependency audit command for your language of choice.
# Node
npm audit
# Python
pip-audit
# Go
govulncheck ./...
# Ruby
bundle audit
Record the count of critical, high, and medium vulnerabilities. For deeper context on how to manage the dependency tree safely, read dependency management without the chaos.
Scan for Secrets
Run a secret scanner against the repository history.
gitleaks detect --source . --redact
Hardcoded secrets in git history are one of the most common findings in a legacy audit. If you find any, flag them for immediate rotation regardless of whether the audit is complete.
Step 3: Read Version Control History
The git history is the single richest source of information about a legacy codebase. It records every change, every author, and every file that has changed together.
Find the Hot Spots
Hot spots are the files that change most often. They are where the business logic lives and where risk concentrates.
git log --format='%H' --since='12 months ago' \
| xargs -I{} git diff-tree --no-commit-id --name-only -r {} \
| sort | uniq -c | sort -rn | head -30
This gives you the top 30 files by change frequency over the last year. Expect a Pareto distribution: a small number of files account for the majority of changes. Those files deserve the deepest reading.
Identify Knowledge Silos
git shortlog -sne --since='12 months ago'
This lists contributors by commit count. If one person accounts for 80 percent of recent commits, you have a knowledge silo. That is a bus factor risk worth flagging.
Find Orphaned Code
Look for files that have not been touched in a long time.
git ls-tree -r HEAD --name-only \
| while read f; do
echo "$(git log -1 --format='%ai' -- "$f") $f"
done \
| sort
Files untouched for three or more years are either extremely stable or dead code. Both are worth investigating.
Step 4: Map the Architecture
You need a mental model of how the system hangs together. Build it by tracing a single request end to end.
Pick a common user journey, ideally one that touches multiple layers. Start at the entry point (HTTP handler, queue consumer, CLI command) and follow the code all the way to the database or external API. Note every component the request passes through.
Repeat this exercise for two or three different journeys. You will quickly spot patterns: which layers exist, how data flows, where side effects happen, and which components are shared across flows.
Sketch the result as a simple box and arrow diagram. It does not need to be exhaustive. The goal is a picture you can put in front of a teammate and have them nod.
Step 5: Run Static Analysis
Static analysis tools scan source code for known problems without executing it. They catch entire categories of issues that manual reading misses.
| Tool | What It Covers |
|---|---|
| SonarQube ↗ | Bugs, code smells, duplication, security hotspots |
| Semgrep ↗ | Pattern based linting, custom rules, security scanning |
| CodeScene ↗ | Hot spots, complexity, technical debt scoring |
| CodeQL ↗ | Deep semantic analysis, security vulnerabilities |
Pick one or two and run them against the codebase. Do not try to fix the findings during the audit. Record the totals and note any critical issues that need urgent attention.
For more on automating these kinds of checks as part of your normal workflow, read automating code quality with linters and formatters.
Step 6: Assess Test Coverage and Quality
Coverage numbers on their own are misleading. A module with 90 percent coverage can still be poorly tested if the tests only cover happy paths or assert on shallow outputs.
Spend an hour reading the existing tests. Ask:
- Are tests isolated or do they share state?
- Do they assert on behaviour or implementation details?
- How fast is the suite? A 45 minute test run discourages people from running it locally.
- What happens if a test fails? Is the failure clear?
If you find large sections of code without any tests, that is a priority area to flag. As a starting point, consider adding characterisation tests before anyone tries to change those modules. Our article on how to write tests that actually help covers the patterns that work best for retroactively adding tests.
Step 7: Interview the Humans
Code tells you what the system does. People tell you why.
Schedule short conversations (30 minutes is plenty) with anyone who has historical context:
- Previous maintainers, even if they are on another team
- Support engineers who have debugged incidents
- Product managers who remember why certain features exist
- Customers or internal users who depend on the system
Ask open questions. What do you wish you could change? What breaks most often? Which part would you not touch? What feature looks redundant but is actually critical?
This kind of institutional knowledge is invisible in the code and impossible to recover once the people leave. Capture it in the audit report while you can.
Step 8: Write the Report
The audit is only useful if someone reads it. Keep the report short, scannable, and focused on actions.
A structure that works well:
- Scope and method. What you audited, what you did not, how long it took.
- System overview. A short description and the architecture diagram from Step 4.
- Risk summary. Top five to ten risks in priority order. Include security issues, fragile components, knowledge silos, and outdated dependencies.
- Hot spots. The files and modules that change most often and deserve the most attention.
- Quick wins. Changes that are low risk and high value, typically completable in a few days.
- Strategic recommendations. Longer term suggestions, such as breaking apart a module, replacing a library, or running a dedicated upgrade project.
- Open questions. Things you could not answer within the time box.
Aim for five to ten pages. If you cannot summarise a codebase in that space, the report will not be read.
Step 9: Agree the Next Steps
The audit itself is not the outcome. The decisions that come out of it are. Walk through the report with your team or manager and agree on:
- Which quick wins to start on immediately
- Whether any critical issues need an incident response (security vulnerabilities, hardcoded production secrets)
- What longer term work to propose
- Who owns each action
This is also the moment to decide on approach. Are you going to maintain and gradually improve the existing system, apply the strangler fig pattern to replace it piece by piece, or accept it as it is and invest elsewhere? The right answer depends on the risks you surfaced and the value the system delivers. Our article on technical debt, when to fix it and when to leave it is a useful companion when making that call.
Common Pitfalls to Avoid
Audits go wrong in predictable ways. Watch out for these.
- Analysis paralysis. If you cannot finish the audit in your time box, cut scope. A done audit with gaps is infinitely more useful than a perfect one that never ships.
- Starting with judgement. The code may look terrible, but it has been serving users for years. Understand before you criticise.
- Skipping the humans. Tools show you patterns. People tell you causes. You need both.
- Producing a wishlist. A good audit recommends a small number of prioritised actions, not 50 ideas with no ranking.
- Ignoring the audit afterwards. If the report sits in a folder unread, the work was wasted. Make sure someone owns the follow up.
A Minimal Audit Checklist
If you only have a week, here is the shortest useful audit you can run.
- Get the application running locally and document the setup steps.
- Run
cloc, your language’s dependency audit, and a secret scanner. - Pull out the top 20 hot spot files from git history.
- Trace one user journey end to end and sketch the architecture.
- Run one static analysis tool and record the headline findings.
- Review test coverage and read a sample of existing tests.
- Interview two or three people with historical context.
- Write a five page report with risks, quick wins, and recommendations.
- Walk the team through it and agree on next steps.
That is enough to turn an unfamiliar codebase into a known one, with a clear plan for what to do next.
Final Thoughts
Legacy code analysis is a skill that pays compounding returns. Every codebase you audit teaches you something you can apply to the next one. The engineers who do it well are methodical, patient, and more interested in understanding the system than in rewriting it.
Before you touch a single line, spend a week mapping the terrain. You will find problems earlier, ship changes more safely, and build the kind of reputation that gets you trusted with the harder systems next time. Combine the audit discipline here with the refactoring patterns in our working with legacy code guide, and you have a complete playbook for turning inherited chaos into a system you can confidently improve.
Frequently asked questions
What is legacy code analysis?
Legacy code analysis is the practice of systematically examining an existing codebase to understand its structure, dependencies, risks, and areas of change. It combines static analysis tools, version control history, dependency audits, and manual reading of hot spots. The goal is not to judge the code but to build an accurate map before making decisions about maintenance, refactoring, or replacement.
How long does a legacy codebase audit take?
A focused audit of a medium sized service can usually be completed in one to two weeks by a single engineer. Larger monoliths may need several weeks and multiple people. Time box the exercise so it produces an actionable report rather than a never ending research project. You do not need to understand every line; you need to understand the risks, the hot spots, and the safest places to start making changes.
What tools help with legacy code analysis?
Start with the tooling you already have. Git log, cloc, and your IDE's call hierarchy view cover most of what you need. Add a static analysis tool like SonarQube, Semgrep, or CodeScene for automated findings. Use npm audit, pip-audit, or govulncheck for dependency risk, and a dead code detector like Knip or deadcode for unused exports. Most of these are free and take minutes to run.
What should a legacy code audit report contain?
A good audit report covers: a high level architecture diagram, a list of hot spots from version control history, a dependency risk summary, a test coverage snapshot, known security issues, and a prioritised list of recommended actions. Keep it short enough that stakeholders will actually read it. Five to ten pages is usually plenty. Include a clear recommendation on where to start.
Should I audit before refactoring or rewriting?
Yes, always. Jumping into refactoring or rewriting without an audit is how teams end up months behind schedule. The audit tells you which parts of the system are load bearing, which are safe to change, and which are best left alone. A one to two week audit up front typically saves many weeks of rework later.
Enjoyed this article? Get more developer tips straight to your inbox.
Comments
Join the conversation. Share your experience or ask a question below.
No comments yet. Be the first to share your thoughts.