A Great Programmer Removes, Doesn’t Add
Discover why true programming mastery lies in the courage to remove and simplify code, fighting technical debt, feature creep, and dependency bloat for healthier, more maintainable software.
Writing software often seems like a straightforward act: open an IDE, an editor, or even just Notepad, and start typing. You don't always need detailed specifications; sometimes, an idea and the will to achieve a result are enough. And, inevitably, something starts to take shape.

The first "Hello World" is always a magical moment, but it's just the beginning. Once this initial hurdle is overcome, one discovers that software development is a continuous process of learning and adaptation. Even if it's complicated at first, over time you grasp the fundamentals: frameworks, libraries, and design patterns. You learn how to structure and modularize code, and how to test it. Eventually, after days or weeks of work, you achieve a working solution—perhaps not exactly as initially envisioned, but functional. This moment often brings an explosion of adrenaline and satisfaction.
Even the most pessimistic individuals find solutions after hours, days, or weeks of effort. Previously, friends and forums were key resources. Today, we can even rely on artificial intelligence or intelligent agents, simply writing our idea, even ungrammatically, to initiate the magic.
However, today I want to shift focus. Let's set aside the romanticized view of programming—the popular belief that merely touching a keyboard can produce incredible things—and not dwell on how software is built or how results are achieved. The world is already saturated with courses and manuals on those topics.
What I would like to discuss is one of the most complex and least-examined aspects of programming: the courage to remove code.
The Real Challenge Is Not a Working Program
Delivering a working program is only the first step in software construction. Most programmers, confident in their abilities, will always assert they can do it. They might be a bit vague about timelines or actual goals, but their answer will consistently be: "I can do it." (I'm excluding chronic pessimists who immediately declare things "impossible" for practicality).
The true challenge, therefore, isn't just delivering functional software. It's about not being overwhelmed by your own creation—not losing control, and preventing a trivial 10-line REST service from evolving into a disk-eating monster (I speak from personal experience).
Software growth is inherently entropic, and an expert programmer's job is to actively combat this entropy.
Code is often likened to an asset, but in reality, it's a liability. If we view the code we write as an asset, we miss the most fundamental aspect of the entire creation process: writing code creates debt. Every line of code must be read, understood, maintained, tested, and, eventually, updated or removed.
Less code means a smaller attack surface for bugs and lower maintenance costs.
This principle is often underestimated, especially in corporate environments where the emphasis is on "delivering features" rather than on "maintaining healthy and manageable code."
Those who work for enlightened product companies understand the critical importance of maintenance, removing obsolete code, and effective dependency management. This is because software is never truly "finished"; it is a living organism that evolves, grows, and sometimes falls ill. When this vision is absent, a lack of planning eventually leads to inevitable collapse.
I have had the fortune and misfortune to participate in both types of projects: some focused solely on delivery, which over time imploded into uncoordinated masses of code; others where continuous refactoring, obsolete code removal, and maintaining a clean, maintainable codebase were standard practice. The difference between these two approaches is profound.
We Are All Serial Hoarders
Do you know the show "Hoarders"? It features people who accumulate useless objects, filling their homes until they become unlivable. Software suffers from the same pathology.
Whether we write it or, worse, an AI does, code inevitably increases in volume. Initially, it's just a few classes, functions, and scripts, but over time we find ourselves deluged not only with our own code but also with external dependencies, libraries, modules, and plugins. I admit I've fallen into this trap multiple times, insisting that every repository I create be self-contained with all necessary dependencies to function—I've seen projects stall due to the lack of a single library.
Newer developers might not fully grasp this yet, but over time they will realize that AIs primarily aim to produce working code and, if it isn't, to convince themselves that it is.

The ultimate goal of an AI is not to optimize code or reduce its complexity. This aspect is currently entirely delegated to us humans. If humans don't prioritize this problem, AIs certainly won't worry about it for us.
This is where our role fundamentally changes. Generative AIs are powerful "addition factories," capable of writing thousands of lines in seconds. This shifts the human role from "writer" to "editor" or "curator." Our most important skill will become the ability to validate, simplify, and reject AI-generated code, not just to produce it. Therefore, it's crucial to guide AI-powered tools effectively: it's not enough to say "generate a CRM"; we must consider the architecture, dependencies, and future maintenance, then break down the task into smaller, manageable chunks for the AI.
To gauge if you are teetering on the edge of a cliff or already in free fall, try this: inspect the node_modules folder of one of your projects, or those created by Maven or NuGet. In a relatively simple project of mine, I have 1 MB of my own code and 20 MB of node_modules. The feeling of it getting out of hand is very strong, and I'm only at the beginning of the project.
Have you ever wondered: "Why does my three-line 'Hello World' require 200 MB of dependencies?" If not, I urge you to. You will discover a new, often underestimated world that can transform a simple project into a maintenance nightmare.
Chatting with other programmers, I often hear phrases like this:
I didn't switch from Vue 2 to Vue 3 because I have 18 dependencies that I can't update without rewriting half the project.
It may sound like an excuse, but it's a reality many developers confront daily.
Dependencies are often integrated too lightly; we accumulate a mountain of features without real justification. We add, and add, and then add again. It's easy to incorporate a new feature, especially if the request comes from a client or product manager. But rarely do we pause to ask: "Is this feature truly necessary?" And, critically, we almost never backtrack.

This phenomenon has a name: feature creep.
Feature Creep: The Silent Enemy
The question we must ask ourselves every day, when we see a line of code lingering for years, is: "Is it really necessary?" The answer is often "no." But acting on that "no" is costly. Explaining it to a client or a product manager requires solid technical and verbal justification, which few have the courage or strength to sustain.
"Feature creep" is not just a code problem; it's a product one. A user interface cluttered with too many buttons, options, and configurations suffers from the same issue. The courage to "remove" also applies to the Product Manager who says "no" to a new feature to keep the product focused and simple.
Also, be wary of those who tell you:
The problem is your fat client, not the dependencies you use.
This is merely a tactic to divert attention from the actual problem: the uncontrolled accumulation of code and dependencies. It's not that one architecture inherently has an advantage over another; the problem of serial accumulation remains constant.
Being meticulously critical, a fat client could have advantages over a thin client segmented into N services, given that it represents a single point of control compared to verifying services used by an virtually uncontrolled number of clients—but that's a different discussion.
Superfluous features and out-of-control dependencies complicate everything. Software becomes fragile, slow, and difficult to maintain. Technical debt grows invisibly until it becomes impossible to ignore.
The Nightmare of Maintenance
As long as we are actively "focused on the project" and daily engaged with features and refactoring, we have a better chance of managing the chaos. However, inevitably, if a project is set aside and resumed after months, or if the team experiences significant changes, the unavoidable drift toward chaos and increased technical debt begins.
The higher the number of imported dependencies, the greater the problem of cross-cutting obsolescence.
But by "dependencies," we shouldn't just think of library X or Y used to parse a JSON file. Consider the entire ecosystem: language versions, libraries, frameworks, toolchains, build systems, execution environments, and so on.
Sometimes you find yourself in a technical cul-de-sac: a necessary update conflicts with another fundamental library that hasn't been updated or whose maintainer has vanished. It matters little if it's open source—this too is an overestimated concept in software development.
At that point, choices are limited: rewrite parts of the software, accept the security vulnerability, or leave everything as is and hope it doesn't collapse.
It's easy to claim "my software is perfect." This might be true for the lines you wrote—even if often just a bias—but it's certainly not true for the ecosystem of external components it relies upon.
Why Don’t We Remove?
So, why do we hesitate to remove? Because we are afraid. Afraid of breaking something (e.g., "What if this code was needed for that specific case X that I haven't tested?").
To remove with courage, two fundamental things are required: technical and psychological safety, but above all, the genuine will to do it.
A corporate culture that rewards subtraction is also essential. In many companies, productivity is measured by "features delivered" or "tickets closed." But how often is a programmer praised for removing 1000 lines of obsolete code? A Pull Request showing -1000 lines should be celebrated. We need a culture that views refactoring and cleaning not as "chores," but as an integral and noble part of the development process.
The Courage to Remove
The real programmers are those capable of reducing.
Ken Thompson, co-creator of Unix and the C language, once famously said:
One of my most productive days was throwing away 1000 lines of code.
I don’t know how literally true this statement is, but the underlying concept is powerful. True mastery lies not in adding, but in knowing how to remove without losing functionality.
There is also a famous quote by Tony Hoare, the inventor of Quicksort:
There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult.
How can you argue with him? After all, his degree in classics, prior to his computer science career, perhaps gave him an edge in understanding the elegance of simplicity.
However, since there are no roses without thorns, in addition to this gem, Hoare also left us one of the greatest tragedies of modern computing: the concept of the "NULL POINTER." If it ever ruined your day, now you know who to thank.
Only the Brave Remove Stuff
But back to the practicalities. How do we, concretely, remove?
The first step is to remove useless code. This sounds easy, but it's where structural and human resistances kick in. To win this battle, adequate tools and mindsets are needed:
- Robust automated tests: A comprehensive test suite provides confidence that if something essential is removed, a test will fail, protecting against unexpected regressions.
- Psychological safety: A work environment where "breaking" something in a test or staging environment is not a crime, but a recognized part of the learning and cleaning process.
- Continuous usage verification: How many features were implemented for a client who is no longer with us? How many procedures are no longer used by anyone? It is fundamental to ask these questions and act accordingly.
- Facing the fear of "what if it's needed one day?": This is the most common excuse for not deleting. Spoiler: that day will almost never come. And even if it did, requirements will likely have changed so much that rewriting the code from scratch will still be the best option, rather than adapting an old and forgotten solution. Code written for a "future implementation" is a mortgage on the future that is rarely redeemed.
The next step is to simplify complex logic and minimize dependencies. There are exemplary cases of software designed with zero or very few dependencies, precisely to facilitate distribution and maintenance.
Every self-respecting software project should dedicate time to elimination and cleanup, treating it like a sacred ritual. Every line of code is a weight, a responsibility we carry. Every line we add is a line we will have to maintain, test, and debug.
It's not a catastrophe to delete. With versioning systems like Git, we can always recover what has been removed. But the truth is that, nine times out of ten, that code was just dead weight.

True Mastery Is Simplicity
Adding is easy. Everyone is adept at adding. But removing? That's where the real game is played.
The mastery of a senior developer is measured not by how much code they can write, but by how much code they can eliminate while preserving essential functionalities.
What holds us back is the fear of "I won't remove it because maybe it will be needed tomorrow." But that "tomorrow" almost never comes. As the YAGNI principle (You Aren't Gonna Need It) states: if you don't need it now, don't implement it.
True value lies not in adding, but in distilling the essential, eliminating the noise, and building something that truly works. This is the courage to remove. And this is what differentiates an enlightened programmer from someone who doesn't see past their own weekend.