Learn how to delete code without fear.

•

•

The Code Is in Git, Delete It

I once spent three days tracing a memory leak in a legacy logistics service, only to realize the culprit was a “safety net” function written by a developer who left the company in 2019. We were all too terrified to touch it because nobody actually knew what it did, so we just let it sit there, bloating the heap and slowing down every transaction. Most people will tell you that you need massive, expensive test suites or complex static analysis tools to solve this, but they’re missing the point. Learning how to delete code without fear isn’t about buying more tooling; it’s about building a culture where context is treated as a first-class citizen and knowing exactly which ghosts you’re exorcising.

I’m not here to sell you on some shiny new testing framework or a theoretical way of organizing your repositories. I want to talk about the practical, slightly messy reality of pruning a codebase that has grown too large for its own good. I’ll share the specific, boring-but-effective habits I’ve picked up from years of production incidents—the kind of stuff that actually helps you rip out a dead service on a Tuesday afternoon without needing to stay up until 2:00 AM fixing the fallout.

Table of Contents

Identifying Unused Functions Before They Break Everything

Identifying Unused Functions Before They Break Everything

The problem with most automated code analysis tools is that they are too optimistic. They’ll tell you a function is unused because there’s no direct reference to it in your current repository, but they won’t tell you that a legacy cron job or a forgotten internal dashboard relies on that specific endpoint to keep the billing cycle running. I’ve spent enough time in production to know that “unused” is often a matter of perspective. Before you reach for the delete key, you have to look beyond the immediate call stack.

My approach is to treat identifying unused functions as a forensic investigation rather than a cleanup task. I start by checking the logs—real, live production logs—to see if that specific logic has actually been executed in the last thirty days. If the telemetry is silent, I’m more confident, but I still don’t trust it blindly. I usually run a grep across the entire infrastructure, including our deployment scripts and configuration files, because code often lives in the gaps between services.

Once I’m reasonably sure the code is dead, I don’t just vanish it. I use a staged approach to refactoring legacy code safely. I’ll first wrap the function in a heavy layer of logging or a feature flag. If I can toggle it off and nothing screams in Slack, then—and only then—do I actually commit the deletion. It’s slower, but it beats the alternative.

Automated Code Analysis Tools to Stop the Guesswork

Automated Code Analysis Tools to Stop the Guesswork

You can’t rely on your memory alone, especially when you’re staring at a monolith that’s been accumulating cruft since the Obama administration. I used to think I could just “eyeball” a module and know what was safe to prune, but that’s how you end up with a production incident involving a dependency that was only called by a single, obscure cron job. Using automated code analysis tools isn’t about being lazy; it’s about building a safety net so you aren’t playing a guessing game with the production environment.

I usually start with something basic like `unused` or specialized linters that catch dead code paths, but the real value comes when you integrate these into your CI pipeline. If you want to succeed at refactoring legacy code safely, you need to move beyond simple syntax checks. I’ve found that combining static analysis with runtime coverage reports is the only way to truly see which branches are actually being executed. It’s not a silver bullet, but it shifts the conversation from “I think this is dead” to “the telemetry proves this hasn’t been touched in six months.” It turns a terrifying deletion into a calculated technical decision.

Five ways to stop sweating every pull request

  • Don’t trust the grep. I’ve spent too many hours chasing a string that looked unused only to realize it was being called via a dynamic reflection trick in a library we haven’t touched in three years. If you can’t trace the call stack through the entire lifecycle, leave it alone for now.
  • Write the test before you delete the code. If you’re removing a logic block, write a test that specifically asserts the behavior that code was supposed to handle. If the test passes without that code, you’ve actually proven it’s dead; if it fails, you just saved yourself a 3 AM rollback.
  • Check the telemetry, not just the source. Sometimes code is “unused” in the repository but is actually a fallback mechanism triggered only when a specific third-party API returns a 503. If your error rates stay flat after the deletion, you’re probably safe.
  • Use feature flags as a safety net. For the really scary stuff—the core logic that everyone is afraid to touch—wrap the removal in a toggle. It’s better to have the overhead of a flag than the adrenaline spike of a production outage.
  • Leave a breadcrumb trail. When I delete something, I leave a comment or a link to the Jira ticket explaining why it was safe to remove. Six months from now, when someone else looks at that empty space and wonders if they should put it back, they’ll have the context they need to stay calm.

The Post-Mortem Checklist for Deleting Code

Don’t trust your intuition; trust the telemetry. If you can’t trace a function’s usage through logs or distributed tracing, it isn’t “unused,” it’s just “silent,” and silent code is the kind that wakes you up at 2 AM.

Always write the “why” in your commit message before you touch the logic. If you’re deleting a block of code that looks redundant but was actually a workaround for a weird edge case in a legacy payment gateway, you need that context recorded so you don’t accidentally resurrect the bug six months from now.

Treat deletion as a deployment, not a housekeeping chore. Small, incremental purges followed by a period of observation are much safer than a massive “spring cleaning” PR that introduces a dozen regressions you didn’t see coming.

The Art of Letting Go

The Art of Letting Go: Deleting code.

At the end of the day, deleting code isn’t a housekeeping chore; it’s a defensive maneuver. We’ve covered how to move from blind guessing to using actual data, whether that’s through static analysis or tracing runtime dependencies to ensure you aren’t cutting a branch that’s still holding up the tree. The goal isn’t to have the shortest file in the repository, but to have a codebase where every line has a clear, documented reason for existing. When you combine automated tooling with a healthy dose of skepticism about “just in case” logic, you stop being a developer who is afraid of the delete key and start being one who manages complexity instead of just accumulating it.

I still occasionally feel that slight hesitation before hitting enter on a large PR that removes several hundred lines of logic. That instinct is actually useful—it keeps you from being reckless. But don’t let that hesitation turn into paralysis. The most stable systems I’ve ever worked on weren’t the ones with the most features; they were the ones where the engineers had the discipline to prune the dead wood regularly. Clean code is a moving target, and you’ll never truly finish the job, but if you approach every deletion as a way to reduce your future cognitive load, you’ll find that the fear eventually disappears.

Frequently Asked Questions

How do I handle code that isn't called by our services but is still used by some obscure, undocumented cron job or a manual script?

This is the “ghost in the machine” problem. I’ve spent way too many hours chasing a “dead” function only to find some developer’s bespoke script from 2019 running it once a month. Don’t trust your IDE’s call hierarchy alone. I’ve started treating the infrastructure as part of the codebase. If you can’t find the caller in the repo, check the crontabs, the CI pipelines, and the logs. If it’s being called, there’s a trace. Find the trace, or don’t touch it.

What’s the safest way to roll back if I delete a "dead" dependency and realize it was actually a critical part of a legacy edge case?

The only real safety net is a clean git state and a deployment pipeline that doesn’t take forty minutes to run. Before you touch the dependency, make sure your current branch is a clean slate. If the “dead” code turns out to be a vital piece of legacy logic, you don’t debug—you revert. I’ve learned that trying to “fix” a broken rollback in a panic is how you turn a minor incident into a full-blown outage.

How do I convince a product manager that spending a sprint cleaning up old logic is more important than shipping a new feature?

Stop talking about “technical debt”—it sounds like a vague tax to a PM. Instead, frame it as a velocity problem. I tell them that every line of dead logic we leave behind is a tax on every new feature we ship. If we don’t clear the brush now, the next “simple” feature will take three weeks because we’re tripping over old, half-broken logic. It’s not about tidiness; it’s about maintaining our ability to ship.

At what point does "cleaning up" turn into "refactoring for the sake of it," and how do I know when I've gone too far?

I know I’ve gone too far when I’m changing logic that isn’t actually broken. If I can’t point to a specific bug, a performance bottleneck, or a piece of dead weight, I’m just rearranging deck chairs. Refactoring should be a response to a problem, not a way to cure boredom. If my pull request doesn’t include a “why” that relates to stability or clarity, I’m probably just playing with the code.

Tomasz Wierzbicki

About Tomasz Wierzbicki

I am not interested in arguing about frameworks. I am interested in why the thing broke at two in the morning and what would have prevented it. Most of what I know came from being wrong in production and writing it down afterwards.