Taking Over a Legacy Codebase: The First Two Weeks

A practical playbook for inheriting someone else's web project: what to read before touching code, how to map the risky paths, and why the first deploy should be deliberately boring.

Taking Over a Legacy Codebase: The First Two Weeks

Sooner or later every studio inherits a project someone else built. The original developer is gone, the documentation is a README from 2019, and the client needs a "small change" that has to ship this month. The temptation is to start coding immediately. The better move is to spend the first days understanding what you're standing on.

Read before you run

Before the first change, I want three things: a local environment that boots, a way to see what production looks like, and the commit history. The history matters more than people expect. git log --oneline --since="2 years ago" -- <directory> tells you which parts of the code are alive and which are zombies nobody has touched since the launch. Code that changed last month is where the client's money is; code untouched for two years is often dead weight you can ignore, or eventually delete.

The README is a starting point at best. What usually documents the real system better:

  • The deployment script or CI config, which shows how code actually reaches production.
  • The .env.example (or the server's config), which lists every external service the app talks to.
  • The database schema, especially foreign keys and enum columns. The data model outlives the framework code around it.

Map the risky paths first

Every inherited app has a handful of paths where a bug costs real money or data: payment processing, auth, password resets, anything that writes without a confirmation step. I walk through those paths manually before anything else, in a staging copy, and write down what I find. Just notes at this stage: "checkout recalculates totals client-side," "user model has two soft-delete flags."

This gives you two things. Landmines surface early, while the client still thinks of you as the new person rather than the person who broke the billing. And you get an honest baseline: when the client later asks "is this codebase as bad as it feels?", you can answer with specifics instead of a shrug.

Write tests for behavior, not coverage

Inherited codebases usually have thin test suites, and writing tests for legacy code the way you would for new code is a trap. You don't know the intended behavior; you only know the actual behavior. So characterize what's there:

test('invoice total matches legacy calculation', function () {
    $order = Order::factory()->withLegacyItems()->create();
    expect(app(TotalCalculator::class)->for($order))->toBe(123.45);
});

The number itself doesn't matter. What matters is that when you refactor the calculator three weeks from now, this test tells you whether you changed the output. Lock in the current behavior first, fix the behavior deliberately later, and update the test in the same commit as the fix.

Make the first deploy boring

Resist the urge to make your first deploy a meaningful one. The first deploy's actual job is to verify the pipeline: that your environment variables match, that assets build, that the queue worker restarts, that cron entries run under the right user. Ship a change so small it can't break anything, a translation string or a comment fix, and watch everything around it instead of the change itself.

If the deploy goes sideways, you've learned the deploy system is fragile, and you learned it before your first real change was riding on it. If it goes cleanly, you now have proof the pipeline works and a rollback path you've actually exercised.

Change the code slowly, change the ops knowledge fast

The code itself rarely needs to change quickly. The knowledge around it does. Within the first two weeks, make sure someone on your side can: deploy from scratch, restore the database from backup, and rotate the third-party credentials the app depends on. Once those three abilities exist, an inherited project stops being a mystery box.

After that, the rhythm is the same as any healthy project: small changes, tested behavior, honest communication about what you found. Legacy codebases earned their reputation through neglect, and once the deploy works and the risky paths are documented, the word "legacy" stops meaning much.