Old features feel harmless until someone tries to change the system around them. They slow releases, confuse staff and keep data you still have to protect. This guide is for owners and technical leads who need to remove a feature without a surprise outage.
Short answer: Retire a feature in three steps. First turn off event listeners and hide the UI so nothing new is written. Then remove leftover code stubs once nothing calls them. Wait about two weeks of quiet, confirm nothing still needs the data, and only then drop the tables in their own migration. Until that last step, you can still undo with a redeploy.
Why is removing a feature riskier than building one?
Removing a feature is riskier than building one because dependencies are often invisible until they break. A screen may look unused while a nightly job, a report or another service still reads the same tables. A single release that deletes UI, code and data at once leaves you with one way forward and no easy rollback.
This is the contract side of Parallel Change (also called expand, migrate, contract). That pattern is usually described for introducing a new interface safely. The same discipline applies when you take something away: shrink behavior first, migrate callers, then remove what is left. The migrate phase for a removal is the quiet period where you watch for anyone who still needed the old path.
For business owners, the cost of keeping dead features is real: slower changes, more training and a larger surface to secure. The cost of a rushed drop is also real: downtime, lost history and a scramble to restore from backup. A staged retirement is how you get the savings without betting the company on one deploy.
Step 1: turn off listeners and hide the UI
Step 1 stops new work from entering the feature. Remove or disable event listeners, webhooks, scheduled jobs and screens that create or update the feature's data. Users should no longer see controls that write to those tables. Integrations that used to fire into the feature should stop receiving or sending those events.
What you are aiming for:
- No new rows written by product paths you control.
- No new user actions that depend on the feature.
- Logs or metrics that prove the write path is quiet.
Keep the read path and the tables in place for now. Someone may still need a report, an export or a support lookup. Hiding the UI without deleting the data is how you learn who still cares.
If you use feature flags, this is a good place for one: hide the UI and skip listeners while the tables remain. Flipping the flag back is cheaper than restoring a dropped schema.
Step 2: remove the stubs
Step 2 removes the leftover code once you are confident nothing calls it. After listeners and UI are gone, search for remaining functions, API handlers, client methods and dead feature flags. Delete them in a normal code review, not in the same change that drops tables.
Tips that keep this step calm:
- Prefer compiler and search tools over memory. If a symbol still has callers, fix or redirect those callers first.
- Remove public API endpoints only after clients have moved or been turned off.
- Keep a short note in the pull request of what you checked (call sites, jobs, dashboards).
Until the database drop, a mistake here is still recoverable with a redeploy. That is the point of ordering: reversible steps first, irreversible last.
Step 3: drop the tables after a quiet wait
Step 3 drops the tables only after a quiet wait of about two weeks (adjust to your release cycle and risk tolerance). Two weeks is a practical default for many product teams: enough time for weekly reports, monthly oddities to start showing, and support tickets to appear if someone still needed the data. Your team may choose longer for regulated data or shorter for purely internal experiments. The rule is the same: wait until the write path has been quiet and you have checked for remaining readers.
Before you run DROP TABLE, check dependencies. In PostgreSQL, DROP TABLE always removes indexes, rules, triggers and constraints that belong to the table. The default is RESTRICT: the command refuses if a view or a foreign-key constraint of another table still depends on it. CASCADE will remove dependent views (and keep walking the dependency chain), which is easy to underestimate in a hurry.
Checklist before the drop:
- Confirm no application path still writes or reads those tables in production logs.
- List dependent views, foreign keys and reporting queries.
- Export or back up anything you might need for compliance or support.
- Put the drop in its own dated migration, reviewed on its own, not buried in a feature PR.
- Prefer
RESTRICTfirst so the database tells you what still depends on the table. Only useCASCADEwhen you intend to remove those dependents too.
Data loss is the one step a redeploy cannot undo. Treat the migration like a production change with a named owner and a restore plan.
A checklist a business owner can ask their team for
You do not need to read the SQL yourself. Ask for clear answers:
| Question | What a good answer looks like |
|---|---|
| What stops new use of the feature? | Listeners and UI are off; screens are gone or flagged off. |
| What still reads the data? | A short list of jobs, reports or support tools, or "none found". |
| How long is the quiet wait? | A named window (for example about two weeks) with what you will watch. |
| What proves it is safe to drop? | Quiet logs, no tickets, dependency check with RESTRICT. |
| Is the drop its own migration? | Yes, dated and reviewed separately. |
| What is the restore plan? | Backup or export location, and who runs the restore. |
If any answer is vague, pause. The feature can stay dark while the team finishes the homework.
How we approach this at Dasnuve
What worked for us as a process is treating feature retirement like any other data change: stop the behavior, remove the code, then remove the data only after a quiet period and a dependency check. We frame this as an open practice rather than a single client story. The exact feature we last retired this way, and what we watched during the wait, are details for the engineer who owns that change to confirm.
If your team is cleaning up systems that feed analytics or warehouses, our guide on what a data pipeline is pairs well with this one. For help designing migrations and retirement plans on AWS-backed systems, see our data engineering services and cloud and AWS work.
FAQ
How long should I wait before dropping tables?
About two weeks of quiet is a solid default for many product systems. Extend it for regulated data, month-end reporting or anything with rare but important readers. Shorten it only when you are sure the feature was never used in production.
Should I use CASCADE when dropping tables?
Only when you mean to remove the dependent objects too. PostgreSQL's default is RESTRICT, which refuses the drop if views or foreign keys still depend on the table. Run with RESTRICT first so you see what is still attached.
Can I delete the UI and the tables in the same release?
You can, but we advise against it. Keep the irreversible step separate. Hide UI and stop listeners first, then remove stubs, then drop tables after a wait. Until the drop, a redeploy can put the feature back.
What is Parallel Change in plain terms?
It is a pattern for making a breaking change in three phases: expand (support old and new), migrate (move everyone to the new path), contract (remove the old path). Feature retirement is mostly the contract phase, with a deliberate migrate window where you watch for leftover users.
Do I need a backup if the feature was unused?
Yes, if the tables still hold history anyone might need for support, audits or disputes. Unused in the product UI does not always mean unused in reporting. Export first when there is any doubt.
Key takeaways
- Stop listeners and hide UI before you delete code or data.
- Remove stubs only after callers are gone; that step is still reversible.
- Wait about two weeks of quiet, then drop tables in their own migration.
- Check Postgres dependencies; prefer RESTRICT before CASCADE.
- Ask your team for a written quiet-wait plan and a restore path before the drop.
