You have just been handed a legacy codebase. Maybe you inherited it from a team that left. Maybe your company acquired it. Maybe it is your own project from three years ago, and past-you made some questionable decisions. Whatever the reason, you need to understand what you are working with before you start changing things.
The worst thing you can do with legacy code is start refactoring immediately. Without a thorough audit, you are navigating in the dark. You will fix symptoms instead of causes, break things you did not know existed, and spend months on changes that should have taken weeks.
This checklist gives you a structured approach to auditing any legacy codebase. For each of the 12 points, you will find what to look for, what constitutes a red flag, and how to address the issue. Use it as a systematic evaluation before you write a single line of new code.
1. Dependency Graph Overview
What to check: Generate a visual map of how modules and files depend on each other. This is your architectural X-ray, it shows the actual structure of the code, regardless of what the documentation claims.
What to look for: Start by examining the overall shape of the dependency graph. A healthy codebase has clear clusters with limited connections between them, while an unhealthy one looks like a tangled ball of yarn. Pay attention to the direction of dependencies as well, they should generally flow in one direction (e.g., controllers to services to repositories), and arrows going backward indicate architectural violations. Finally, check the distribution of connections. A few hub nodes are normal (shared utilities, database clients), but too many hubs suggest that boundaries between modules are weak.
Red flags: Watch out for a complete absence of discernible structure where everything depends on everything. A single file with more than 20 incoming or outgoing dependencies is another major concern, as are multiple files importing directly from the database layer across all modules.
How to fix: You do not fix a messy dependency graph all at once. Start by identifying the natural cluster boundaries and documenting them. Then, in future work, enforce those boundaries by preventing new cross-boundary imports. Tools like ReposLens can generate the dependency graph automatically from your repository and track how it evolves over time.
2. Circular Dependencies Count
What to check: Count the number of circular dependency chains in the codebase. A circular dependency exists when module A depends on module B, and module B depends on module A, either directly or through a chain of intermediate modules.
What to look for: You want to find direct cycles (A to B to A), indirect cycles (A to B to C to A), and measure the length of the longest cycle chain.
Red flags: Any circular dependencies in core business logic modules should raise immediate concern. Cycles involving more than 3 modules are nearly impossible to untangle incrementally. More than 5 circular dependency chains in total is a serious problem.
How to fix: For each cycle, identify the weakest link, the dependency that is easiest to break without major restructuring. Common techniques include extracting shared logic into a new module that both sides can depend on, using dependency injection to invert the dependency direction, or introducing an event system or message bus for cross-module communication.
// Before: Circular dependency
// user-service.ts imports from email-service.ts
// email-service.ts imports from user-service.ts
// After: Break the cycle with an event
// user-service.ts emits 'user:created' event
// email-service.ts listens for 'user:created' event
// No direct dependency between the two
3. God Modules (Files With Too Many Imports)
What to check: Identify files that import from an unusually high number of other modules. These are "god modules", they know too much, do too much, and are impossibly difficult to modify without side effects.
What to look for: Focus on files with more than 10 imports from your own codebase (not external packages), files longer than 500 lines, and files whose name includes "utils," "helpers," "common," or "shared" that have grown beyond their original purpose.
Red flags: A single file importing from 15+ modules is a clear god module. A "utils" file over 1,000 lines has long since outgrown its purpose. A service file that handles database queries, email sending, payment processing, and logging is doing far too much for one module.
How to fix: God modules need to be split. The approach depends on the type:
For utility god modules (utils.ts, helpers.ts): Group the functions by domain and split into string-utils.ts, date-utils.ts, format-utils.ts, etc. Update imports across the codebase. This is mechanical and low-risk.
For service god modules (a service that does everything): Identify the distinct responsibilities and extract each into its own service. Use the dependency graph to understand which parts of the codebase depend on which functions, and split accordingly.
4. Dead Code Percentage
What to check: Estimate the percentage of code that is no longer used, exported functions that nothing imports, components that are never rendered, routes that are never hit.
What to look for: Look for files with no incoming dependencies (nothing imports them) that are not entry points, exported functions or classes that have zero consumers, feature flags that are permanently off, and commented-out blocks of code.
Red flags: More than 15% dead code is a serious concern. Entire directories that are unreferenced and test files for modules that no longer exist are both strong indicators of accumulated technical debt.
How to fix: Dead code removal is one of the safest refactoring activities. Start with the obvious wins:
- Delete files with zero incoming dependencies (after verifying they are not entry points, scripts, or dynamically imported)
- Remove commented-out code blocks (they live in git history if you ever need them)
- Remove unused exports identified by your IDE or linting tools
- Remove feature-flagged code where the flag has been permanently off for more than 6 months
5. Test Coverage
What to check: Measure the current test coverage and, more importantly, evaluate the quality of existing tests.
What to look for: Check the overall coverage percentage, then dig into coverage of critical business logic paths (payments, authentication, data transformations). Examine test file freshness, when were tests last updated relative to the code they test? Also assess test quality: do tests actually assert meaningful behavior, or are they snapshot tests that nobody reviews?
Red flags: Less than 30% overall coverage is concerning. Zero coverage on payment or authentication logic is dangerous. Tests that have been disabled or skipped for more than 3 months suggest systemic neglect. Test files that import from modules that have been renamed or moved indicate tests are broken and nobody noticed.
How to fix: Do not aim for 100% coverage on a legacy codebase, that is unrealistic and counterproductive. Instead:
- Write tests for the code you are about to change (the "test before you touch" rule)
- Prioritize coverage on business-critical paths: anything involving money, authentication, or data integrity
- Set a coverage floor for new code (e.g., 80%) and enforce it in CI
- Delete broken or meaningless tests, they provide false confidence
6. Outdated Dependencies
What to check: List all third-party dependencies and their current version relative to the latest available version.
What to look for: Look for major version gaps (e.g., running React 17 when React 19 is out), dependencies with known security vulnerabilities (run npm audit or pnpm audit), dependencies that are no longer maintained (last publish date more than 2 years ago), and duplicated dependencies (two libraries that do the same thing).
Red flags: More than 5 dependencies with known security vulnerabilities is a serious risk. A framework version more than 2 major versions behind means you are missing critical fixes and features. Dependencies that have been deprecated with no migration plan and a package.json with 50+ direct dependencies both signal that dependency management has been neglected.
How to fix: Create a prioritized update plan:
- Immediate: Dependencies with critical security vulnerabilities
- Short-term: Dependencies more than 2 major versions behind
- Medium-term: Deprecated dependencies that need replacement
- Ongoing: Keep a regular cadence of dependency updates (monthly is a good rhythm)
7. Build Time
What to check: Measure the full build time from clean state, as well as incremental build times during development.
What to look for: Measure the clean build time (from scratch), the incremental build time (after a single file change), the CI build time (full pipeline including tests), and note any build steps that seem disproportionately slow.
Red flags: A clean build time over 5 minutes for a project under 100K lines of code is too slow. Incremental build time over 10 seconds disrupts developer flow. A CI pipeline over 15 minutes discourages frequent commits. A single build step that takes more than 50% of total build time is a clear bottleneck.
How to fix: Profile the build to identify the slowest steps (most bundlers have profiling options). Check for unnecessary barrel files (index.ts that re-export everything) since they defeat tree-shaking and slow down builds. Verify that TypeScript's incremental and tsBuildInfoFile options are enabled. Consider splitting the project into workspaces if it has grown beyond monolithic scale, and replace slow tools with faster alternatives (e.g., Biome instead of ESLint + Prettier).
8. Documentation State
What to check: Evaluate the completeness and accuracy of existing documentation.
What to look for: Start with the README: does it explain how to set up, run, and deploy the project? Check the API documentation to see if endpoints are documented and whether the documentation is auto-generated or manually maintained. Look for architecture documentation that describes the high-level architecture. Review inline comments to see if complex algorithms or business rules are explained. Finally, verify that all required environment variables are documented with descriptions.
Red flags: A README that has not been updated in over a year is a problem for any new contributor. API documentation that references endpoints which no longer exist is actively misleading. The absence of a .env.example file, architecture diagrams that do not match the actual code structure, and comments like // TODO: fix this or // temporary hack that are more than a year old all indicate documentation debt.
How to fix: Documentation triage, fix what matters most:
- Update the README with correct setup instructions (this blocks every new contributor)
- Create or update
.env.examplewith all required variables - Generate API documentation from source (OpenAPI/Swagger) rather than maintaining it manually
- Delete outdated architecture diagrams (wrong docs are worse than no docs)
- Address TODO comments: either do the task or remove the comment
9. Error Handling Patterns
What to check: Examine how the codebase handles errors, both expected (validation failures, not-found) and unexpected (network failures, runtime crashes).
What to look for: Check whether the codebase uses consistent error handling patterns or a mix of approaches. Look for generic catch blocks that swallow errors, error messages that expose internal details to users, error boundary or fallback UI for frontend applications, and whether errors are logged with sufficient context for debugging.
Red flags: Empty catch blocks like catch (e) { } that silently swallow errors are among the most dangerous patterns. Equally problematic are catch (e) { console.log(e) } blocks where errors are logged to console with no alerting. Inconsistent error response formats across API endpoints, no global error boundary in the frontend, and error messages containing stack traces, file paths, or database queries visible to users all indicate poor error handling discipline.
// Red flag: Silent error swallowing
try {
await processPayment(order);
} catch (e) {
// TODO: handle this
}
// Red flag: Inconsistent error responses
// Endpoint A returns: { error: "Not found" }
// Endpoint B returns: { message: "Resource not found", code: 404 }
// Endpoint C returns: { errors: [{ msg: "not found" }] }
How to fix:
- Define a standard error format for the entire application
- Replace empty catch blocks with proper error handling (log, retry, or propagate)
- Add a global error boundary (React ErrorBoundary, Express error middleware)
- Ensure errors are reported to a monitoring service (Sentry or equivalent)
- Sanitize error messages sent to clients, never expose internal details
10. Security Vulnerabilities
What to check: Audit the codebase for common security issues.
What to look for: Search for hardcoded secrets (API keys, database credentials, tokens), SQL injection vulnerabilities (string concatenation in queries), cross-site scripting (XSS) vectors (unsanitized user input rendered as HTML), missing authentication on protected routes, missing authorization checks (authenticated but not authorized), and sensitive data in logs or error messages.
Red flags: API keys or passwords in source code are a critical issue, even if they are in config files that are gitignored, because they may have been committed previously. Raw SQL queries with string interpolation, dangerouslySetInnerHTML without sanitization, routes in the dashboard that do not check authentication, and missing CORS configuration or overly permissive CORS (Access-Control-Allow-Origin: *) are all serious security concerns.
How to fix:
- Run a secrets scanner (
gitleaks,trufflehog) on the full git history - Rotate any exposed credentials immediately
- Replace string-concatenated SQL with parameterized queries or an ORM
- Add authentication middleware to all protected routes
- Implement proper CORS policies
- Set up security headers (CSP, X-Frame-Options, X-Content-Type-Options)
11. Performance Bottlenecks
What to check: Identify areas where the application performs poorly under load or with large data sets.
What to look for: Watch for N+1 query problems (fetching a list, then querying for each item individually), missing database indexes on frequently queried columns, unoptimized images or assets (large images served without compression or resizing), memory leaks (event listeners not removed, subscriptions not cancelled), and blocking operations on the main thread (heavy computation in UI code).
Red flags: API endpoints that take more than 1 second for common operations indicate performance issues. Database queries without WHERE clauses on large tables, a frontend bundle size over 1 MB (gzipped) for a standard web app, no pagination on list endpoints, and loading entire datasets into memory when only aggregates are needed are all signs of performance neglect.
How to fix:
- Add database indexes on columns used in
WHERE,JOIN, andORDER BYclauses - Fix N+1 queries by using eager loading or batch queries
- Implement pagination on all list endpoints and list UIs
- Add image optimization (compression, responsive sizes, lazy loading)
- Profile the frontend bundle and code-split large dependencies
12. Architecture Boundary Violations
What to check: Verify that the codebase respects its own architectural boundaries, the intended separation between layers, modules, and domains.
What to look for: Look for UI components importing directly from the database layer, business logic embedded in UI components or API route handlers, cross-domain imports (e.g., the "orders" module importing internal functions from the "users" module instead of going through a public API), and configuration and environment variables accessed from arbitrary locations instead of through a centralized config module.
Red flags: A React component that contains a database query is a clear boundary violation. API route handlers longer than 50 lines suggest business logic that should be in services. More than 10 import paths that cross domain boundaries and no clear separation between what is "public API" of a module and what is internal both indicate weak architectural discipline.
How to fix:
- Document the intended architecture boundaries (which layers exist, what each layer is allowed to import)
- Move business logic from UI components and route handlers into service modules
- Create clear public APIs for each domain module (an
index.tsthat exports only what other modules should use) - Enforce boundaries with linting rules (eslint
import/no-restricted-pathsor equivalent) - Use architecture analysis tools to track boundary violations over time, ReposLens can visualize exactly where cross-boundary imports occur and alert you when new ones are introduced
Summary Table
| # | Check | Green | Yellow | Red | |---|-------|-------|--------|-----| | 1 | Dependency graph | Clear clusters | Some tangling | Everything depends on everything | | 2 | Circular dependencies | 0 | 1-3 | 4+ | | 3 | God modules | None over 10 imports | 1-2 files | 3+ files with 15+ imports | | 4 | Dead code | < 5% | 5-15% | > 15% | | 5 | Test coverage | > 60% on critical paths | 30-60% | < 30% | | 6 | Outdated deps | All within 1 major version | 2-3 major behind | Security vulnerabilities | | 7 | Build time | < 2 min clean | 2-5 min | > 5 min | | 8 | Documentation | Up-to-date README + env | Partial docs | No docs or wrong docs | | 9 | Error handling | Consistent patterns | Mixed approaches | Silent failures | | 10 | Security | No known issues | Minor issues | Hardcoded secrets or SQL injection | | 11 | Performance | Sub-second responses | Some slow endpoints | N+1 queries, no pagination | | 12 | Boundary violations | Clean separation | Some leaks | No boundaries enforced |
How to Prioritize
Not all 12 items carry equal weight. Here is a suggested prioritization:
Fix immediately (blocks all other work): Security vulnerabilities (#10) and missing or broken documentation for setup (#8) should be addressed before anything else, because they either put you at risk or prevent anyone from contributing.
Fix before any refactoring: You need a dependency graph overview (#1) because you need to see the terrain before you move. Circular dependencies (#2) will undermine any refactoring effort if left unaddressed. Test coverage on code you plan to change (#5) is essential so you can refactor with confidence.
Fix as part of ongoing work: God modules (#3) should be split as you encounter them. Dead code (#4) should be removed when you find it. Error handling (#9) can be standardized as you touch each module, and boundary violations (#12) should be enforced on new code while fixing existing code gradually.
Fix as separate initiatives: Outdated dependencies (#6), build time (#7), and performance bottlenecks (#11) are important but can be tackled as dedicated efforts outside your main refactoring work.
Conclusion
Auditing a legacy codebase is not glamorous work, but it is the most valuable thing you can do before refactoring. Every hour spent on this checklist saves days of wasted effort later.
The goal is not to fix everything at once. The goal is to understand what you are working with, identify the highest-risk areas, and create a plan that addresses the most impactful problems first. Print this checklist, score your codebase on each item, and use the results to build a refactoring roadmap that your team can execute with confidence.
Related Articles
Microservices vs Monolith: How to Actually See Your Architecture
Monolith or microservices? The real question is: can you see what you actually have? Learn how to visualize, compare, and decide with confidence.
Continue readingHow to Detect Circular Dependencies in Your TypeScript Project (and Fix Them)
Learn 3 practical methods to detect circular dependencies in TypeScript: madge CLI, ESLint import/no-cycle, and automated PR checks with ReposLens. Includes fix patterns.
Continue readingMonorepo Dependency Management: Visualize Before It's Too Late
Learn how to manage internal dependencies in a monorepo. Discover common pitfalls like circular deps and god packages, and tools to visualize your architecture.
Continue reading