How I Evaluate an Unfamiliar Codebase Before Making Major Changes
A practical codebase evaluation process for senior engineers who need to understand risk, architecture, tests, data, and delivery before making major changes.
The first mistake people make in an unfamiliar codebase is trying to prove they are smart too quickly.
I understand the temptation. You open the repository, see naming you would not choose, folders that look out of date, a few long files, maybe a framework version from another era, and the rewrite thoughts start forming before the app even runs. That reaction is common. It is also expensive.
A working codebase is not only code. It is a record of product decisions, team habits, delivery pressure, customer edge cases, and incidents that happened before you arrived. Some of it is messy because nobody cared. Some of it is messy because the problem itself is messy. You need to know the difference before making major changes.
When I join a project or take ownership of a large change, I treat the first evaluation as risk discovery. The goal is not to understand every line. The goal is to understand where change is cheap, where change is dangerous, and what evidence I need before touching the critical parts.
This is the process I use.
Start with the business shape
Before reading much code, I want to know what the software is responsible for.
That sounds obvious, but engineers often start with folders, dependencies, and architecture diagrams. Those matter, but they mean less until you know what the business cannot afford to break. A strange implementation around billing deserves different attention than a strange implementation around a rarely used admin filter.
I usually ask a few plain questions:
- What does this product do for customers?
- Which workflows create revenue, trust, or legal exposure?
- Which users are most affected when the system is slow or wrong?
- What parts of the product receive the most support tickets?
- What is the major change we are considering, and why now?
The answers change how I read the repository. If the project is a SaaS platform, I look for tenant boundaries, subscription logic, permissions, background jobs, and integrations. If it is a marketplace, I look for payments, inventory, disputes, notifications, and audit trails. If it is an internal tool, I look for data correctness, access control, and manual recovery paths.
This step also keeps me honest. A codebase can look unpleasant and still serve customers reliably. Another codebase can look modern and still fail in production because nobody modeled retries, large accounts, or partial failure. The shape of the business tells me where technical beauty is useful and where correctness matters more.
I like to leave this step with a short list of protected workflows. For example:
- New customer signup.
- Subscription upgrade and downgrade.
- Payment provider webhook processing.
- Team invitation and permission changes.
- Data import for a large account.
That list becomes the lens for the rest of the evaluation.
Get the system running before judging it
I do not trust my opinion of a codebase until I can run it.
Reading code without running the system is like reviewing a map without knowing which roads are closed. You can learn structure, but you miss friction. The setup process tells you what new engineers experience and which parts of the project exist only in someone's memory.
I start with the normal path:
- Read the README.
- Install dependencies using the documented command.
- Copy the example environment file if one exists.
- Run the development server.
- Run the test suite.
- Run linting or type checks if they exist.
I pay attention to every failure. Missing environment variables, outdated setup notes, seed data that does not work, and tests that pass only on one person's laptop are not small details. They tell me how much hidden knowledge the team carries.
I do not immediately fix every setup problem. I take notes first. If I fix while exploring, I can accidentally erase evidence about the real onboarding cost. Later, I may submit a small setup improvement as the first safe change.
The most useful setup questions are practical:
- Can a new engineer run this within an hour?
- Does the project tell me which services are required?
- Are external dependencies mocked, seeded, or documented?
- Do tests fail because the code is broken or because the environment is unclear?
- Is the local development path close enough to production to expose real issues?
A codebase that is hard to start is not automatically bad, but it slows every future change. If I am planning a major migration, setup quality becomes part of the risk estimate. You cannot safely refactor what the team cannot reliably run.
Map the entry points and data flow
Once the app runs, I map how work enters the system.
I am not trying to draw a perfect architecture diagram. I want a practical map that shows where requests, jobs, commands, scheduled tasks, and external events come from. This helps me avoid changing a function that looks local but is actually called by three critical workflows.
In a web application, I look for:
- Routes and controllers.
- API handlers.
- Middleware.
- Background jobs and queues.
- Scheduled commands.
- Webhook endpoints.
- Event listeners.
- CLI scripts or one-off maintenance commands.
Then I follow the data. Which tables matter? Which models or query builders touch them? Which fields are trusted as source of truth? Which data is copied into another system? Which values can be recalculated, and which values must be preserved exactly?
Data flow matters because major changes often fail at boundaries. The code compiles, the unit tests pass, and then production reveals that one webhook sends duplicate messages, one job runs during deployment, or one old table field still feeds a report used by finance.
I also look for places where the code hides side effects. A method called updateUser might send an email, create an audit log, sync a CRM contact, and dispatch a billing event. None of those side effects are wrong by default. They are only dangerous when they are invisible.
The map I want at this stage is simple:
- How does the workflow start?
- Which modules does it pass through?
- Which data does it read and write?
- Which side effects happen?
- What happens after the request finishes?
That map is usually enough to guide deeper reading.
Find the places where change is dangerous
Every codebase has hot zones.
Sometimes they are obvious: billing, authentication, permissions, migrations, data deletion, checkout, import pipelines. Sometimes they are hidden in boring names like helpers.js, BaseService, sync.php, or legacyAdapter. I spend time looking for these areas before proposing changes.
Danger usually comes from one of five sources.
First, the code has many callers. A shared helper, base component, middleware, or utility may look small, but changing it can affect the whole product. I use search heavily here. If a function has broad usage, I treat it as shared infrastructure even if nobody named it that way.
Second, the code handles irreversible side effects. Payments, emails, external API updates, deletes, and legal records require more care because rollback is incomplete. You can revert a deploy. You cannot unsend an email or pretend a customer was not charged twice.
Third, the code depends on timing. Queues, scheduled jobs, retries, cache expiry, background workers, and concurrent requests can behave differently under production load than they do locally. These areas need idempotency and observation.
Fourth, the code has weak ownership. If nobody can explain why a module works the way it does, I assume there is hidden risk until proven otherwise. This does not mean the code is bad. It means the team has lost context.
Fifth, the code is close to customer data. Anything that changes stored data needs more evidence than a UI-only change. Database migrations, backfills, and schema changes deserve a release plan, not just a pull request.
I mark these zones explicitly. A major change becomes much easier to plan when the dangerous areas are visible. It also improves the conversation with product and leadership. Instead of saying "this code is messy," I can say "this change crosses billing webhooks, a destructive migration, and a queue with no duplicate processing guard."
That is a better technical argument.
Read tests as a map of team confidence
Tests tell me two things: what the system is supposed to do, and what the team is afraid to break.
I start by running the full suite if practical. Then I read tests near the protected workflows. I am looking for coverage shape, not just coverage percentage.
Useful tests usually describe business behavior:
- A user without permission cannot access a resource.
- A duplicate webhook does not create a second charge.
- A subscription downgrade applies at the correct time.
- A failed import leaves the previous data intact.
- A migration keeps old and new application versions compatible.
Weak tests often test implementation without protecting behavior. They mock so much that the system under test cannot fail in the same way production fails. They assert that a method was called, but not that the customer-visible result is correct.
I do not dismiss weak tests with arrogance. Tests often reflect the pressure the team was under. A thin test suite may exist because the product changed quickly, the original team was small, or the architecture made testing painful. The useful question is not "why did they do this badly?" The useful question is "what confidence do we need before the next major change?"
For major work, I look for three kinds of missing confidence:
- Characterization tests around current behavior.
- Integration tests around important boundaries.
- Regression tests for known incidents or support issues.
Characterization tests are especially useful in unfamiliar systems. They let me capture what the system does now before I change it. The current behavior may not be ideal, but writing it down prevents accidental changes from hiding inside a refactor.
If tests are slow or flaky, that becomes part of the plan. A slow suite encourages risky shortcuts. A flaky suite teaches engineers to ignore failure. Both reduce the team's ability to change the system safely.
Trace one real workflow end to end
After the broad map, I pick one real workflow and trace it all the way through.
I prefer a workflow related to the proposed major change. If we are changing subscriptions, I trace an upgrade. If we are replacing a frontend area, I trace the API calls, permissions, state, and backend writes that support that area. If we are extracting a service, I trace the data ownership and side effects around that boundary.
This is slower than skimming files, but it exposes the truth of the system quickly.
For each step, I ask:
- What input is accepted?
- Where is it validated?
- What authorization check protects it?
- Which data is read?
- Which data is written?
- What side effects are triggered?
- What happens when a dependency fails?
- What does the user or support team see?
I also look at names while tracing. Good names reveal the domain. Bad names force me to keep too much in my head. A class called SubscriptionChangePreview tells me more than BillingHelper. A function called applyScheduledDowngrade tells me more than processPlan.
This is where I often find the first useful refactoring targets. Not huge rewrites. Small changes that clarify the path: splitting a validation rule from a side effect, renaming a misleading function, adding a missing test, or moving repeated permission logic into one visible place.
The key is discipline. I do not let a workflow trace become an open-ended cleanup mission. The goal is to understand the change path and identify risk. Cleanup earns its place only when it reduces that risk.
Look at operational reality
A codebase is only half the system. The other half is how it behaves after deployment.
Before major changes, I want to know how the team deploys, observes, and recovers the application. This is especially important in SaaS products where the same code may serve many tenants with very different data shapes.
I look for:
- Deployment process and release frequency.
- Feature flag usage.
- Database migration strategy.
- Rollback limits.
- Background worker behavior during deploys.
- Logs, metrics, traces, and alerts.
- Runbooks for common failures.
- Recent incidents and support patterns.
The interesting part is not whether the tooling looks fashionable. The interesting part is whether the team can answer basic production questions.
If a payment job starts failing, who knows? If one tenant has ten times the normal data volume, where will that show up? If a migration locks a table, how quickly can the team detect and stop it? If a third-party API slows down, does the app fail safely or does every request pile up behind it?
Major changes often require operational changes. A database redesign may need new dashboards and a backfill plan. A new queue may need dead-letter handling. A permission model change may need audit logs. A frontend migration may need feature flags and a way to compare error rates between old and new paths.
I like to connect every risky technical change to an observation plan:
- What signal should improve?
- What signal would show harm?
- Who checks it after release?
- What action do we take if it goes wrong?
Without those answers, the team is not shipping a change. It is shipping a guess.
Make the first change small on purpose
My first change in an unfamiliar codebase is usually boring by design.
I want to learn the real delivery path before carrying a high-risk change through it. A small change reveals review habits, CI behavior, deployment timing, ownership boundaries, and the team's appetite for risk. It also builds trust because I am improving the system without pretending to understand everything yet.
Good first changes include:
- Fixing a setup instruction that blocked local development.
- Adding a characterization test around a workflow I just traced.
- Improving a misleading name in a narrow area.
- Adding logging around a known failure point.
- Removing dead code after confirming no usage.
- Tightening validation for a specific edge case.
The first change should be easy to review and easy to revert. It should teach me something about the system and leave the codebase slightly safer than before.
I avoid first changes that touch global abstractions, rewrite folder structure, change formatting across the repository, or introduce a new framework. Those changes may be justified later, but making them too early creates noise and hides the real risk.
This is also where I learn the team's review culture. Do reviewers focus on behavior or style? Do people know the areas they approve? Does CI catch meaningful problems? Are deploys routine or dramatic? Those answers affect the plan for the major work more than any architecture diagram does.
When the first small change lands cleanly, I have better evidence. When it gets stuck, I have learned something important before the stakes are high.
Write down the codebase risk map
I finish the evaluation by writing a short risk map.
This does not need to be a formal document. One or two pages is often enough. The purpose is to turn scattered observations into a plan that other people can challenge.
My codebase risk map usually includes:
- The protected workflows.
- The main entry points and data flow.
- The risky modules and why they are risky.
- The test coverage that exists.
- The confidence gaps before major changes.
- The operational risks and missing signals.
- The safest first increments.
- The decisions that need product, engineering, or operations input.
The document should separate facts from judgement. "The subscription webhook has no duplicate event test" is a fact. "This makes the billing migration high risk" is judgement. Both are useful, but mixing them makes the conversation weaker.
A good risk map also protects the team from vague rewrite energy. If the codebase needs serious change, the document should show why. If the change can be done incrementally, it should show that too. The point is not to make the old system look foolish. The point is to choose the next move with enough evidence.
This is where senior engineering ownership shows up. You are not just reading code. You are reducing uncertainty for everyone who depends on the change.
Frequently asked questions
Common questions about evaluating unfamiliar codebases
How long should a codebase evaluation take?
For a small application, a focused first pass can take one or two days. For a larger SaaS product, I usually expect several days before making major architectural recommendations. The goal is not total understanding. The goal is enough evidence to identify the risky areas and plan the first safe increments.
Should I refactor while evaluating the codebase?
Take notes first. Small improvements are useful after you understand the path they affect, especially setup fixes, tests, and local clarity improvements. Avoid broad refactors until you know the callers, data flow, deployment path, and production risk.
What is the biggest red flag in an unfamiliar codebase?
The biggest red flag is not old technology or messy folders. It is a critical workflow that nobody can explain, nobody can test, and nobody can observe in production. That combination makes change risky because the team has little evidence before or after release.
How do I evaluate a codebase with almost no tests?
Start by tracing protected workflows and adding characterization tests around current behavior. Focus on the parts involved in the next major change. You do not need to solve the entire test strategy before making progress, but you do need enough coverage to detect the most expensive mistakes.
When is a rewrite justified?
A rewrite may be justified when incremental change cannot reasonably reduce the risk, cost, or delivery constraint that matters. That conclusion needs evidence: failed attempts to isolate change, operational limits, data model constraints, or product needs the current system cannot support. A rewrite based only on code style frustration is usually a bad bet.
