# How to delete code safely: find who depends on it first

> A practical process for removing dead code, unused services and stale feature flags without breaking production, from first inventory to final removal.

- URL: https://byteminus.com/blog/how-to-delete-code-safely/
- Published: 2026-10-15
- Author: Byte Minus Engineering (Byte Minus)
- Topics: Refactoring, Engineering practice

**In short:** To delete code safely: list each candidate with a reason it may be unused; gather evidence from code references, runtime logs and the people affected; add real tests where the suite is hollow; deprecate with logging and a feature flag before removing; delete in small, ordered pull requests; and write down what was removed and why.

Every mature codebase carries weight it no longer needs: feature flags that were rolled out years ago, endpoints no client calls, a service that was "temporarily" kept alive, a cron job nobody owns. Teams usually know it is there. What stops them from deleting it isn't effort — it is fear of what might be depending on it.

Deleting code is a big part of how we build at Byte Minus, and the deleting is the easy part. The work, and the part that makes it safe, is **finding out who depends on the code first**. Here is the process we follow.

## 1. Make an inventory, not a hunch

Start by writing down the candidates for removal, each with a reason it might be dead:

- feature flags that have been at 100% (or 0%) for months;
- routes and endpoints with no recent traffic;
- classes, functions and modules with no references;
- jobs, queues and scheduled tasks;
- database tables and columns nothing reads;
- dependencies imported in the manifest but not in the code.

A spreadsheet is fine. What matters is that each item gets an owner, a reason, and a place to record evidence.

## 2. Gather evidence from three places

"I don't think anything uses this" is a hunch. Before removing anything, look for evidence in all three of these:

### Static references

Search the code, of course, but also the places code search misses: configuration files, infrastructure definitions, database migrations, CI pipelines, documentation, and **other repositories** that might call this one. Dynamic dispatch, reflection, string-built class names and serialised job payloads all hide references from a simple search.

### Runtime evidence

Code with no references can still be reached. Check access logs for the routes, metrics for the jobs, and query logs for the tables. If the tooling doesn't exist, add it: a log line or a counter on the code path you want to remove costs little and turns a guess into data. Then **wait long enough to cover your business cycle** — month-end jobs and annual reports are the classic surprise.

### People

Ask the teams who might be affected. Support teams know which "unused" admin screens they open once a quarter. Finance knows which report is pulled for the auditors. This is often the fastest source of evidence, and the most often skipped.

## 3. Check what your tests actually protect

Before removing code, you want tests that will fail if the removal breaks something real. Unfortunately, many suites contain tests that would pass even if the code beneath them were deleted — tests that assert a file exists, mock the very thing they claim to test, or check nothing after calling a function.

We use an internal tool, Untester, to find these on every audit; mutation-testing tools do a similar job in most languages. Knowing which tests are hollow tells you where you are actually unprotected — and those are exactly the places to add a real test before you delete anything nearby.

## 4. Deprecate loudly before you delete

For anything with a chance of hidden callers, don't go straight from "present" to "gone":

1. **Log** every use of the code path, with enough context to find the caller.
2. **Warn** callers you can reach — a deprecation header on an API, a log message, a note in the changelog.
3. **Brown out** where it is safe: switch the feature off for short, announced windows and watch what breaks.
4. **Disable** it behind a flag, so turning it back on is a configuration change rather than a deploy.
5. **Delete** once the flag has been off for a full business cycle with no complaints.

Each step is reversible, and each produces more evidence than the one before.

## 5. Remove in small, reviewable steps

When it is time to delete, keep each change small: one flag, one endpoint, one table per pull request. Small removals are easy to review, easy to revert, and easy to bisect if something goes wrong a week later.

Delete in dependency order — callers first, then the thing they called, then its data. Drop database columns and tables last, and only after a backup that you have actually restored once.

## 6. Write down what the deleted code used to do

The final step is the one that makes the next deletion easier: a short record of what was removed, why, what evidence showed it was unused, and where to find it in version control if it is ever needed again. A paragraph in the pull request description and a line in the changelog are usually enough.

This record is what turns deletion from a risky one-off into a routine the team trusts.

## A checklist you can copy

- [ ] Candidate listed, with an owner and a reason it may be dead
- [ ] Static references checked, including config, infrastructure and other repositories
- [ ] Runtime usage measured over a full business cycle
- [ ] Affected teams asked
- [ ] Hollow tests identified; real tests added where needed
- [ ] Deprecated with logging, then disabled behind a flag
- [ ] Removed in small, ordered pull requests
- [ ] Removal documented

## The payoff

Every line removed is a line nobody has to read, test, upgrade or secure again. Teams that delete routinely move faster, onboard people more easily and have fewer places for bugs to hide.

We follow this process on our own products and open-source packages. It is the reason they stay small.
