Engineering

How to decommission a feature safely

Editorial · Reveneau · September 27, 2026

How to decommission a feature safely

Every roadmap we look at has a column for building things. Almost none have a column for taking things out.

That imbalance has a cost that grows every month without anyone noticing. A feature that was released four years ago and is used by nobody still consumes dependency upgrades, still appears in the security patch queue, still adds minutes to the test suite on every commit, and still takes some of the attention of every person who has to reason about the system. It costs something every single month and returns nothing.

Removal is the only operation that makes that total smaller. Refactoring moves work around. Documentation describes it. Deleting is the only thing that ends it.

So why does nobody schedule it? Because it has no launch, no demo, and no metric that moves in a direction anyone celebrates. It is also genuinely risky if done carelessly, and one bad experience teaches a team never to try again. The method below is designed so that a bad experience costs you an afternoon rather than an incident.

Step 1: prove it is unused, and define unused precisely

The most common failure is measuring one thing and concluding another. Zero page views is a different statement from zero API calls, which is different again from zero rows written, which is different from zero rows read.

Instrument all of them. Every route, every endpoint, every scheduled job that touches the feature, every table it owns. Add a log line at each entry point with enough detail to identify who is calling: user or service identity, timestamp, and the parameters used. That last part matters more than it looks, because "twelve calls last month" is an unusable fact while "twelve calls last month, all from one internal script owned by finance" is a complete answer.

Then wait longer than feels necessary. Business software has cycles: monthly closes, quarterly reports, annual renewals, the audit that happens every February. A feature can be silent for three months and then support a critical process on one day. We treat a full quarter as the minimum observation window for anything customer-facing, and longer if the feature has any connection to reporting or compliance.

One more check that costs an hour and saves a week: search your support tickets and your sales notes for the feature name. Usage data tells you what is happening. It does not tell you that the feature was promised in a contract signed last March.

Step 2: find the dependencies you cannot see

Direct callers are the easy part. Search the codebase, follow the imports, done in twenty minutes.

The dependencies that cause problems are the ones with no reference in your repository at all. Five categories cover most of them.

Shared database tables. Another service reads or writes a table this feature owns, and nothing in your code shows it. Check for real connections at the database level rather than trusting the code, because a query from a second system leaves no trace in your source. This is the single most common surprise.

Scheduled jobs. A cron entry, a queue consumer, or a workflow in some orchestration tool that nobody has looked at since the person who wrote it left. These are frequently defined outside the repository, which is exactly why they get missed.

Data exports. Someone in finance built a spreadsheet on a CSV this feature generates, and that spreadsheet is now essential to a monthly process. There is no technical signal for this at all. You find it by asking people.

Permissions and roles. Roles defined specifically for this feature, or permission checks that other code reuses because it was convenient. Removing the feature can silently widen or break access somewhere unrelated.

Retained data. Whatever the feature wrote is probably still subject to retention rules, legal hold, or a customer's right to have it deleted. That obligation continues after the feature is gone. The pattern of copies lasting longer than the system that made them is covered in retention and deletion across copies.

Write all of this down as a list before touching anything. If the list has an entry you cannot resolve, that entry is the project, and the removal waits.

Step 3: remove in stages you can reverse

This is the part that turns a risky operation into a routine one. Five stages, each reversible except the last, each with a waiting period that is long enough to reveal a problem.

Stage one: hide it. Remove it from navigation, menus, and search. Leave every code path working. Anyone with a saved link or an existing integration continues without interruption, and casual usage stops. Reversing this takes a minute. Wait two to four weeks.

Stage two: return an error, and log everything. Remaining callers now get a clear failure with a message telling them who to contact. Log every call with full identifying detail. This stage is a way to find unknown users as much as a removal step: the traffic that arrives here is the traffic that genuinely exists, and now you know exactly whose it is. Wait four weeks, longer if any monthly or quarterly process could be involved.

Stage three: stop writing. The feature no longer creates new data. Existing data stays exactly where it is and remains readable. This is where you find out whether anything downstream depended on those rows continuing to appear, which is a failure mode that only shows up when the rows stop. Wait a full cycle.

Stage four: delete the code. Feature code, its tests, its configuration, its dependencies, its permissions, its documentation. Do it in one commit with a clear message explaining what was removed and why, because that commit is the record someone will need in two years. This is reversible from version control, which is precisely why it comes before the step that is not.

Stage five: delete the data. Last, separately, and only when retention rules allow it. Restoring a dropped table is harder than restoring a deleted function.

The value of this sequence is that you can stop at any point without embarrassment. If stage two produces three angry messages within a day, you have not caused an incident. You have learned that the feature is used, you have learned who uses it, and you learned both at the cost of one reverted configuration change. That is a good outcome, and calling it one is what makes a team willing to try again.

What to do about the feature that is rare but real

Sometimes the investigation ends with a feature used twice a year by four customers, and both of those uses matter.

Rarity and value are separate questions. A compliance report generated once a year can be the most important thing your system does. The right choice then is to reduce the cost rather than remove the capability: remove its dependencies, simplify the code path down to the one case that is actually used, remove the configuration options nobody sets, or replace it with a manual process if the volume genuinely supports that. Four uses a year is a number a person can handle.

That decision needs both a product owner and an engineer involved. Engineering knows what is expensive to maintain. Product knows which small group would be badly hurt. Neither has all the information, and the failure mode is one of them deciding alone. The broader cost argument is in total cost of ownership and the maintenance view is in maintaining custom software.

Make it a regular item, not a spare-time activity

Removal that depends on spare time never happens, because spare time is the resource a delivery team has least of.

Give it a named owner and a real place on the roadmap: one feature retired per quarter, tracked like anything else you commit to. Attach a number to it that people can see, such as test suite runtime, the count of dependencies you patch, or the number of services in the diagram. Those numbers falling is the only visible reward this work produces, so make sure someone is publishing them.

It matters more now than it did, for a simple reason. When producing code was slow, the codebase grew at the speed of the team. It does not any more. A system that gains features faster than before and removes them at the same rate as before will cost more every year, and the arithmetic is simple. Related thinking is in what is technical debt and when to pay it.

Thanks to the teams who let us help take things out rather than put things in, which is the less glamorous half of the work and often the more valuable one. The pattern we saw again and again: the feature everyone was afraid to remove turned out to be used by one internal script, and the feature nobody worried about turned out to supply the data for a regulator's report.

You cannot know which is which without looking. So look first, then remove in steps you can undo.

Related guide: Maintaining custom software.

Common questions

Why does removing a feature reduce maintenance load when refactoring does not?

Refactoring changes where the work happens while removal makes the total smaller. Every feature that stays in the codebase keeps consuming dependency upgrades, security patches, test suite minutes, and the attention of everyone who has to reason about the system, and only deletion ends those costs permanently.

How do you prove a feature is actually unused?

Instrument every entry point rather than relying on one measure, because zero page views is a different statement from zero API calls or zero database writes. Then wait a full business cycle, since quarterly and annual patterns produce months with no use followed by heavy use on a single day.

What is the most commonly missed dependency during decommissioning?

A shared database table that another part of the system reads or writes. Direct code callers are straightforward to find with a search, but a second service quietly joining against your table leaves no trace in your repository and only appears when the table stops being maintained.

What does a reversible removal sequence look like?

Hide the feature from navigation first, then return an error to remaining callers with logging, then stop writing new data, then delete the code, and delete the stored data last. Each stage waits long enough to reveal a problem, and every stage before code deletion can be reversed in minutes.

How long should you wait between decommissioning stages?

Long enough to cover the longest realistic usage cycle for that feature, which for most business software means at least one full quarter across the whole sequence. A week is only enough for something used daily by a known group, and annual processes need a longer period.

Should you delete the data at the same time as the code?

No, because data deletion is the one step you cannot reverse. Keep the data through the code removal, then delete it once your retention policy permits and once you are confident nothing depends on it, since restoring a table is far harder than restoring a deleted function.

What if a stakeholder objects to removing a feature?

Ask them for a usage number rather than an opinion, and offer to run the reversible sequence which gives everyone real evidence. Most objections come from uncertainty rather than from actual usage, and the hide-it-first stage answers the question at very low cost.

How do you handle a feature that is used rarely but genuinely matters?

Keep it and reduce its cost instead, by cutting its dependencies, simplifying its code path, or moving it to a manual process if the volume is small enough. Rare use and low value are separate questions, and a compliance report used once a year can be the most important thing you run.

Who should decide whether a feature gets removed?

The decision needs a product owner who can weigh customer impact and an engineer who can estimate the ongoing cost, because neither has all the information alone. Engineering usually knows what is expensive to maintain, and product usually knows which small group would be badly affected.

How do you build removal into a regular work schedule?

Give it a permanent place on the roadmap the way you would any other work, with a named owner and a stated target for each cycle. Removal that depends on spare time never happens, because spare time is the resource a delivery team has least of.

What should you log while a feature is being decommissioned?

Every remaining call, with the caller identity, the timestamp, and the parameters used. That record makes the error-returning stage a way to find unknown users, and it is what tells you whether the traffic still arriving is one forgotten script or a customer workflow you did not know about.

Start a project