The Craft of Deleting Code
Adding code is easy. Knowing what to remove - and having the conviction to remove it - is where the real craft lives.
There is a specific kind of pride that comes from writing a lot of code. It is visible in pull requests that span hundreds of lines, in engineers who describe their projects by the size of the codebase, in the quiet satisfaction of a git log that shows thousands of commits attributed to your name.
It is almost entirely misplaced.
The engineers I have most admired - across companies, teams, and domains - share a different orientation. They take as much satisfaction from a pull request that deletes five hundred lines as from one that adds them. They understand that code is a liability as much as an asset, and that the most powerful thing you can often do is find the lines that should not be there and remove them.
This essay is about the craft of deletion: why it is hard, how to develop the instinct for it, and what you see differently once you start practicing it seriously.
Why Deletion Is Hard
Adding code feels safe. You understand what you are adding. The tests you write confirm it behaves the way you intend. The diff shows a clear increase in capability. When the feature ships, there is evidence of your contribution.
Deleting code carries different psychological weight. The code exists for a reason - or it once did. Someone wrote it. Maybe you wrote it. Removing it feels like it might break something, even if no tests exercise it. It feels presumptuous. Who are you to decide this code is unnecessary?
This feeling is worth examining, because it is the primary reason codebases accumulate dead weight. Not laziness. Not ignorance. The legitimate psychological difficulty of asserting that something that exists should not exist.
The engineers who learn to delete well learn to manage this discomfort. They develop a set of questions that make the decision less dependent on feeling and more dependent on evidence:
- Is this code exercised by any test?
- Is this code reachable from any entry point?
- Has this code path been triggered in production in the last 90 days?
- Would anyone notice if it were gone?
If the answer to all four is no, the burden of proof has shifted. The question is no longer "should I delete this?" but "why shouldn't I?"
The Four Categories of Code Worth Deleting
Not all deletable code looks the same. Here is the taxonomy I have found most useful.
Dead code. Functions that are never called. Branches that are never taken. Exports that are never imported. These are the easiest to identify with static analysis tools - a TypeScript strict build, eslint's no-unused-vars, Go's unused import checker. They are also the easiest to delete because there is no ambiguity: the code does not run.
Commented-out code. This category is pernicious because it masquerades as documentation. Commented-out code is not documentation. It is code that someone was not confident enough to delete, left as a note to a future reader who cannot interpret it without knowing the history. If the code existed, the commit history contains it. Delete it.
Defensive code for conditions that cannot occur. This is subtler. It includes null checks for values your type system guarantees are non-null, fallback branches for error conditions your infrastructure prevents, and retry logic in codepaths that never fail transient failures. This code is not wrong - it might even be correct - but it obscures the code that matters and creates cognitive overhead for everyone reading the function.
Abstraction that serves hypothetical future requirements. The third version of something that has only ever been instantiated once. The plugin interface for a system that has never had more than one implementation. The configuration knob that nobody has ever turned. Build for what exists, not for what might exist.
Before
After
The before version is not wrong. Every guard has a story behind it - a bug report, a crash, a moment when user was unexpectedly null. But if your type system now guarantees User is non-null at all call sites, those guards are noise. They imply uncertainty that no longer exists, and they make the function harder to read than it needs to be.
The Strangler Fig Pattern in Reverse
The Strangler Fig pattern is a well-known approach to migrating legacy systems: build the new system alongside the old one, gradually route traffic to it, and remove the old system once traffic has fully migrated.
Most teams execute the first two steps well. They consistently fail at the third.
I have seen codebases where the "strangled" legacy system has been running at 0% traffic for two years. It is not deleted because nobody is certain it is safe to delete. The migration was declared complete, the new system was celebrated, and the old code remained - a fossil embedded in the production codebase, confusing every new engineer who encounters it.
The discipline to actually complete the third step - to remove the thing you have replaced - requires organizational will that pure technical instinct cannot provide. It requires someone to own the cleanup, to write the PR, to get it reviewed and merged even when there are shinier things to work on.
This is the craft part. Not the technical knowledge of how to delete safely, but the persistence to see deletion to completion even when it carries no visible reward.
How to Delete Safely
Conviction to delete is necessary but not sufficient. Deleting wrong costs time and trust. Here is how to do it with confidence:
Use your coverage tools to find unreachable code. Istanbul/V8 coverage, Go's cover tool, Python's coverage.py - a coverage report under a meaningful test run will reveal code that has never been executed. Combined with branch coverage, you can identify not just functions but individual branches that are unreachable.
Check production telemetry before deleting. If you have structured logging or metrics, search for the function name, the log line, the error code. A code path that has never appeared in two years of production logs is a candidate for deletion. A code path that appeared three weeks ago is not, even if the tests do not cover it.
Delete in stages for large removals. For major removals - an entire subsystem, a deprecated API - delete in stages across multiple PRs. First mark the code deprecated and add a log warning when it is called. Ship that. Wait two release cycles. If no warnings appear in production, delete. This approach surfaces assumptions you did not know existed.
Trust your type system. If TypeScript, Rust, or Go tells you a function is never called and your linter agrees, the burden of proof has shifted. The compiler has a larger surface area than your intuition.
The Codebase Archaeology Problem
The hardest code to delete is code you did not write and do not understand. This is the codebase archaeology problem: you open a file, see a function, see no callers, and do not know if it is safe to remove.
The answer is usually in the git history. git log -p -- path/to/file will show you every change to a file, including when the code was added, what the commit message said, and what it replaced. A commit message that says "Add fallback for legacy API v1" written four years ago, combined with the fact that the API v1 endpoint was decommissioned two years ago, tells you the story.
Most engineers never look at this history. They treat the current state of the codebase as the primary source of truth and feel blind when they encounter code with no obvious callers. The history is there. Read it.
The Metric That Matters
Here is a metric I have started tracking on my own work: the ratio of lines deleted to lines added over a quarter.
Not because deleting more is always better. Some quarters require adding more. But the exercise of tracking it surfaces a pattern: in quarters where I delete more, the codebase feels lighter at the end. New features take less time to add because there is less to navigate. Reviews are faster. Onboarding is simpler.
The aggregate effect of sustained deletion is a codebase that grows in capability while shrinking in complexity - which is the goal every engineering team says they want and almost none consistently pursue.
Develop the instinct. Trust the compiler. Delete the code.
The follow-up to this essay covers the specific patterns I use for safely removing old API versions - with feature flags, deprecation warnings, and a concrete timeline for final deletion.
Related essays
How I Think About the Staff Engineer Track
Most engineers I know who became staff engineers did not plan for it. The ones who planned for it often got stuck. Here is why - and what the track actually rewards. testing