Frogrammer Logo
Frogrammer.co
Published on

Zen and the Art of Deleting Code: Finding Joy in Letting Go

Authors

Codebases grow. That’s the nature of software. Every feature, fix, and edge case adds to the pile. But over time, that pile becomes a burden. Layers of decisions, half-used modules, and abandoned experiments start to take up mental and physical space. What used to be a clean system becomes messy, intimidating, and harder to change. We stop feeling joy.

I want to tell you a different story. One where deleting code is not a failure, but a sign of maturity. A story where letting go of old code makes room for better software and happier developers. This talk - and now this post - is about finding joy in removal. About building teams and systems that value simplicity. And about getting comfortable with the red lines in your PR diff.

Let’s begin.


Story Time: The Rabbit Hole

It’s your first week on the job. You’re fixing a small bug. You find the function, change the line, and the bug is gone. Simple.

But then you see that the function is used somewhere else. So you follow that trail. And then another. Changing one usage after the other.

You’re hours in now, in a part of the system you don’t know, with code that nobody seems to own. You have a happy hour coming up, and someone just asked you to prepare a toast.

This isn’t about fixing a bug anymore. It’s about untangling a mess.

Eventually, you realize the code you were chasing? It belongs to version one of a feature that’s now in version four. Nobody uses it. It just sat there, alone in the dark, quietly confusing everyone who came after.

That’s when you start to wonder: Why didn’t anyone delete this? Are we doomed to live with this mess?


About Me

I’ve been building web apps for a long time, and I’ve seen what happens to codebases when they grow unchecked. One of the most satisfying moments in my career was deleting 25,000 lines of code from a project - and watching the build pass. That’s what started my romance with code deletion. This led me to a pursue of finding the best ways to do it not as a one time thing, but as a routine, and this post, are the result.


What We’ll Cover

  1. Why deleting code is good for you
  2. How to identify dead code and analyze it
  3. Feature toggles: the good, the bad, and the forgotten
  4. Killing features and products for real
  5. Takeaways you can apply today

Dead Code Analysis: What Are We Even Talking About?

Let’s define it:
Dead code is code that is no longer needed but still exists in the codebase. It’s unused by th users, but maybe there are still unit tests for it from the time it was live.

And here’s the thing: dead code is not neutral. It has a cost. It gets read during reviews. It runs during builds. It needs to compile and pass tests. It can even break things during refactors. Every line of code you don’t need is another piece of weight you carry.

The Gardening Analogy

Think of your codebase like a garden. You plant features. You prune them. Sometimes things grow wild. If you don’t maintain it, weeds show up. Dead code is just weeds that forgot to die quietly.


The 3 Levels of Dead Code

Not all dead code is equally easy to spot or remove. Here’s how I think about it:

Level 1: File Level

  • Unused variables
  • Unused methods
  • Exports that no one imports

Easy to catch with linters and IDE warnings.

:bulb: Tip: Checkout eslint's no-unused-vars and no-unused-exports rules.

Level 2: Module Level

  • Entire files never used
  • Functions called only by other dead functions

Needs a bit more tooling. Static analysis and tools like knip help here.

Level 3: System Level

  • Entire flows behind old feature flags
  • Endpoints that no one hits
  • Legacy UI that is no longer reachable

This level is hard. It requires product knowledge, analytics, and guts.


Example: Deleting 25,000 lines of code

File level

Take this typescript file as an example, and imagine it's part of a much larger codebase.

// demo.ts

import { format } from 'date-fns'

const x = 1

function getMessage(): string {
  return 'Hello'
}

export function getLongMessage(): string {
  return 'Helllllllllo'
}

export function getSpecificiMessage(): string {
  return `Specific Message ${format(new Date(), 'yyyy-MM-dd')}`
}

function getShortMessage(): string {
  return 'Hell'
}

export type Foo = {}

export default {
  getLongMessage,
  getMessage,
}

What are the dead parts in this file that can be deleted safely?

  • x variable is never used. Off with its head!
  • getShortMessage function is never called. It might as well be deleted.
  • Foo type is never exported. It's not even used.

What about the remaning parts?

  • We can see that several function are exported
  • There's a default export to the file However, we don't know if they are used, simply - we don't have enough context. The only way to find out is to look for other modules that might be related, but this is hard to do manually. Luckily, we can use tools to help us.

Module level

Looking at the example above, we can look and see which modules import the demo.ts module, and use it's named exports or default export.

Named export:

// main.ts
import { getLongMessage } from './demo'

Default export:

// main.ts
import demo from './demo'

If we see that one of the exports is not used, we can delete it.

Enters knip - a tool that can help us find unused exports, modules and packages in our codebase.

npx knip

It can be configured to work with advanced build tools like Vite, Webpack and Rollup, and also identify tests code by integrating with Cypress and Playwright.

When running it on the example above, it may return something like this:

Unused exports (2)
getMessage      function  src/demo.ts:2:17
getLongMessage  function  src/demo.ts:6:17

In it find that the entire demo.ts module is not used, and can be deleted, it may say:

Unused files (2)
src/demo.ts

Knip Caveats

  • Needs a bit of configuration to work with different build tools. AI can help here.
  • It's a great tool to get started, but it's not a silver bullet.
  • To make it work better - use named exports instead of default export when exporting modules.
  • It has an auto-fix mode, but it's not perfect.

Tooling for the Win

Here are some tools that help find and fight dead code:

  • knip: Detects unused files and exports in TypeScript projects
  • eslint: Adds rules to surface dead declarations

Culture: Make Deletion Okay

People hesitate to delete code. They don’t want to break something. They think old code is still important. But most of the time, it's just forgotten.

To change this:

  • Celebrate red PRs (more deletions than additions) Celebrate PRs that has more code deletions that additions
  • Make deletion part of onboarding
  • Talk about it in code reviews
  • Teach that code removal is just as valuable as writing new features

And remember: Git has a memory. You can always bring things back.


Feature Toggles: A Double-Edged Sword

Feature flags (toggles) are great. They let you ship incomplete work, experiment, roll out slowly, and hide new features.

But they have a dark side. Too many toggles, or old ones that never get removed, lead to chaos. You end up with multiple versions of logic in the same file. You write tests for both, maintain both, and never feel safe removing anything.

A True Story

We once maintained a flow for weeks, adding tests and tweaking performance, only to discover the feature flag was off for all users. The code was experimental, and no one told us. Three weeks of work for nothing.


How to Manage Feature Toggles

Some advice:

  • Separate temporary toggles from permanent ones
  • Use a “time bomb” system: set a calendar event or build reminder to remove them
  • Document who owns a toggle and when it can be removed
  • Create cleanup sprints for toggles
  • Delete as soon as it’s safe. Git has your back.

Killing Products or Features Completely

Now we get to the big one: removing full features or flows.

Sometimes the code works fine. It’s even tested. But the feature it powers? Nobody uses it. It’s dead, but still standing.

Why Is This So Hard?

Because killing features is more than just code. It’s a product decision. A marketing issue. A sales thing. But if nobody is using something, we should be asking: why is it still there?

How Can We Tell?

Use data. But not vague “feeling” data. Real analytics.

  • Google Analytics
  • New Relic
  • Customer support tickets
  • Logs, events, funnels

As developers, we need to get better at measuring usage. Not just uptime. Actual user behavior.

If no one is clicking it, maybe it shouldn’t exist.


Final Takeaways

  1. Deleting code is productive. You’re making the codebase better
  2. It requires tooling, culture, and courage
  3. Feature toggles should be short-lived
  4. Use analytics to justify removal
  5. Celebrate red PRs - every deletion is a win

Your next PR doesn’t have to be green. In fact, it’s more impressive when it’s red.


Final Thoughts

In the end, deleting code is an act of care. You’re removing confusion for future developers. You’re making systems easier to work with. You’re making space.

And sometimes, the best thing you can do for a project isn’t to add something new - but to let go of what’s no longer needed.

Thanks for reading.