Most codebases do not die from one bad commit. They die from a thousand small ones, each defensible in isolation, that together turn the repo into something nobody volunteers to work on.
Maintenance is the work that keeps that from happening. It is not glamorous. It rarely earns a launch announcement. But the codebases that ship reliably five years in are the ones whose teams treated maintenance as a first-class activity, not a chore to fit in between features.
Below are eleven habits we see in repos that age well, and the failure mode each one prevents.
1. Read the code before you change it
The single most expensive maintenance mistake is editing a file you have not read. Not skimmed. Read. The context behind the function, what calls it, what assumes its current shape.
A change that takes two minutes in isolation can take two weeks to undo when the assumption it broke was load-bearing. Reading first is not slow. It is the only way to make small changes stay small.
In practice this means opening the file in your editor with the call graph visible, running the tests for it once before touching anything, and noting the trail of recent commits to that area. If the function has been stable for two years, treat it accordingly.
2. Keep changes small and reversible
A 600 line PR has roughly four times the probability of containing a regression that survives review than four PRs of 150 lines each. The cost is not linear in size, it is closer to quadratic, because reviewers run out of attention before they run out of diff.
Small changes are cheap to revert. Cheap reverts are the safety net that lets a team move fast. A PR that takes a day to roll back is, in practice, a PR you will never roll back.
The discipline is to break work into the smallest unit that stands on its own. Not the smallest unit imaginable, the smallest unit that compiles, passes tests and can be deployed without breaking anything. Even a refactor of 5,000 lines can usually be sequenced into ten safe steps.
3. Refactor opportunistically, not seasonally
The "refactoring sprint" is a maintenance anti-pattern. Teams batch up the changes they did not make during feature work, freeze the codebase for two weeks, and emerge with a list of merge conflicts and bugs.
Martin Fowler's opportunistic refactoring is the alternative. Each PR leaves the touched area marginally better than it was. Rename one bad variable. Extract one function. Delete one dead branch. Over a year these compound into a substantially better codebase without ever taking a sprint off feature work.
The cultural signal that opportunistic refactoring works: senior engineers do it visibly. Their PRs include the cleanup. Juniors learn the pattern by reading the diffs.
4. Delete code aggressively
A codebase is a liability surface, not an asset. Every file is one more thing to read, test, secure, document and depend on. Code that has not been touched in eighteen months is not "stable", it is "unverified". The runtime behaviour and the developer expectation have drifted.
Run a dead code scanner. knip finds unused exports, files and dependencies across the JS and TS tree. Most repos that have never run it have between fifty and three hundred unused exports. Deleting them is the cheapest maintenance win available, and the one with the biggest morale lift.
The rule we use internally: if you find dead code, delete it in the same PR. Do not file a ticket. Do not "come back to it". The ticket will outlive the codebase.
5. Treat dependencies as inventory, not magic
Every dependency in a package.json is code your team is responsible for, even though you did not write it. Vulnerabilities, breaking changes, licence shifts, abandonment, all of these become your problem.
Two practical habits. First, automate the upgrade flow. Renovate or Dependabot opens PRs as updates land, and the team merges them weekly. Updates that arrive in fives are easy. Updates that arrive in fifties after eighteen months of neglect are a project.
Second, audit at least once a quarter. A short review of the lockfile usually finds two or three dependencies that are no longer needed, plus one or two that have a better, smaller alternative. We covered this in more depth in keeping your dependency supply chain healthy.
6. Pin a linter and a formatter, and never argue
Style debates are the lowest-value conversation a team can have. Pin a formatter (Prettier or Biome), pin a linter (ESLint or Biome), put both in the pre-commit hook via Husky, and move on with your life.
The rule for new rules: add it as a warning for two weeks, fix the existing violations, then promote it to error. Adding a rule as an error on day one creates a 400 line cleanup PR that nobody wants to review and that conflicts with whatever else is in flight.
A tight lint config is a maintenance accelerator. It catches the dumb mistakes before review, freeing reviewers to focus on design, not formatting.
7. Write commit messages for the person who reverts in two years
Most commits are read exactly once, by the reviewer, on the day they land. A small fraction get read again, usually under pressure, by someone trying to understand why a change was made or whether it is safe to revert.
That second reader is who you are writing for. The format matters less than the content. Conventional Commits is a fine default. What matters is that the body of the commit answers "why" rather than restating "what". The diff already shows what changed.
A commit that says "fix bug" is wasted. A commit that says "guard against null user.email after the Stripe webhook started sending guest checkouts with no email field" is gold the next time the same code path moves.
8. Keep the test suite fast and trusted
A test suite has two failure modes. It is too slow, so developers run it less often, so it catches less. Or it is flaky, so developers learn to ignore failures, so it catches less. Both end in the same place: a green CI that does not actually mean anything.
The fix is to budget. Set a wall-clock budget for the unit suite (under two minutes for most repos), enforce it in CI, and quarantine slow or flaky tests rather than tolerating them. A flaky test is a bug in the test, not a fact of life. Fix it or delete it.
We wrote about why the coverage number itself is a poor signal in why your test coverage number is misleading. The point here is upstream of that: a test suite the team does not trust is worse than no test suite, because it consumes time without producing confidence.
9. Document the things only the original author knows
Most code is self-explanatory if you read it carefully. The pieces that are not are the ones worth documenting: the reason a workaround exists, the constraint that drove an unusual structure, the bug that was caught the hard way and the test that prevents it from coming back.
A repo-level ARCHITECTURE.md covering the module layout and the reasoning behind it pays for itself within a quarter, because every new engineer reads it instead of asking the same questions through Slack. We argued the wider case in why documentation is a code quality signal.
The trap is documenting too much. A doc that lists every function is a doc that will be wrong within a month. Document the decisions, not the mechanics.
10. Pay down structural debt before it compounds
Some maintenance debt compounds at near-zero interest. A misnamed variable. A function that is slightly too long. These are fine to leave for now.
Other debt compounds aggressively. Circular dependencies. Tangled module boundaries. A test setup that requires four mocks. These shapes get harder to fix as the codebase grows, because more code is built on top of them. We covered the cycle case specifically in how to fix circular dependencies in JavaScript, and the wider architecture lens in how to improve your codebase architecture.
The triage rule: if the cost of fixing this debt will double in six months, fix it now. If it will stay the same, you can leave it. Most teams underestimate which bucket their debt sits in, and that is where the surprises come from.
11. Measure the trend, not the snapshot
A codebase looks fine on any given day. The question worth asking is whether it is getting better or worse this quarter.
The signals are the ones we use in code quality metrics every team should track: test ratio, duplication, cyclomatic complexity, dependency count, dead code. None of them are decisive in isolation. All of them, tracked over time, show the direction.
The teams that catch maintenance problems early are the ones with a trend line on a dashboard somewhere. The teams that get blindsided are the ones who only look at the codebase when something has already broken. The cost difference between the two is large and visible within twelve months.
What separates the two outcomes
After looking at a few hundred repos, the pattern is consistent. The codebases that age well are not the ones with the smartest engineers, or the most rigorous review processes, or the trendiest tooling. They are the ones whose teams treat maintenance as continuous, not seasonal.
Small PRs. Opportunistic refactors. Aggressive deletion. Automated upgrades. Pinned linters. Trusted tests. Documentation of decisions, not mechanics. Trend lines, not snapshots.
None of these habits is hard. The hard part is doing all eleven at once, every week, for years. The teams that manage it ship the same software with half the headcount and a third of the on-call load of the teams that do not.
If you want a quick read on where your own repo sits, you can connect it to Implera and get a baseline across seven domains in a couple of minutes. The trend line tells you whether the maintenance habits are working.