← All posts

What makes a codebase easy to hand off

We hand off every codebase we build. That's the job. The client needs to be able to maintain it after we're gone, whether that's their own engineers or another studio.

The things that make a codebase hard to hand off are predictable. We see the same patterns. Here's what we do about each of them.

The README answers the right questions

How do I run this locally? What are the main pieces? What are the external dependencies? Not in exhaustive detail, just enough that a new engineer can get oriented in under an hour.

A README that says "clone the repo and run npm start" is not useful. Something breaks, and the person staring at the error has no context for why it might be happening. A README that's 40 pages of documentation for every edge case is also not useful because nobody reads it.

The right length is whatever it takes to answer those three questions clearly. Usually two to four pages.

Environment variables are documented

Somewhere, there's a list of every environment variable the application needs, what it does, and where to get it. We keep this in a .env.example file with every variable present and a comment explaining what it's for and where to find the value.

If a new engineer can't run the project because they're missing a secret, there should be one obvious place to look. "Ask someone who knows" is not a system. It's a single point of failure.

The code does what it says

This sounds obvious. It is, in practice, unusual. It means function names describe what the function does. Variable names describe what the variable holds. If something is called processData, that's not a name. If something is called normalizeUserEmailForLookup, you can guess what it does without reading the body.

The test for this is: can a new engineer, unfamiliar with the codebase, read a function name and roughly predict what it does? If they have to read the implementation every time, the naming isn't doing its job.

Tests cover the critical path

Not 100% coverage. That number is a red herring that optimizes for coverage statistics rather than useful tests. The flows that break the business if they stop working should be covered: payments, auth, the core user-facing feature, the data processing pipeline that runs every night.

The test suite should be a safety net a new engineer can use to tell whether they broke something important when they changed something. If the tests are all testing internal implementation details rather than observable behavior, they don't serve that purpose.

Non-obvious things have a comment explaining why

Not what the code does. Why it does it that way. The what is usually visible from the code itself. The why is what gets lost when context walks out the door with the original developers.

The cases worth commenting: a workaround for a specific third-party bug, a constraint from the database that isn't visible in the code, a performance optimization that looks like it could be simplified but can't be because of a specific production behavior.

Naming is consistent

If the user object is called user in the database, it's called user in the API response, in the frontend types, and in the UI code. Not user in one place, member in another, and account in a third. The same thing has the same name everywhere.

Inconsistent naming is not just an aesthetic problem. It means engineers spend time figuring out whether two things are the same thing or different things, every time they encounter a new part of the codebase. That cost is small per instance and large over the lifetime of a project.

None of this is heroic. It's discipline applied consistently. The cost is low. The payoff when someone new picks it up is high.