
Why experienced developers delete more code than they add
Learn why experienced developers delete code: removing obsolete states, dependencies and abstractions makes software safer and easier to change.
Experienced developers delete code because they have learned what every line becomes after the pull request is merged. It becomes something the team must read, test, secure, deploy, observe and explain. The feature may create value. The code itself creates an obligation.
I did not begin my career thinking this way. Like many developers, I treated visible implementation as proof of progress. A larger diff looked substantial. A reusable abstraction felt more professional than three ordinary functions. Supporting a hypothetical future case felt responsible.
After more than seven years of working with PHP applications, automation and production systems, my instinct has changed. I still write code every day. But the highest-leverage question is often no longer “What should I add?” It is “What can stop existing?”
Good deletion removes more than lines. It removes a behavior nobody needs, a state nobody should reach, a dependency nobody should maintain or a decision future developers should not have to make.
“Delete more than you add” is not a productivity metric
The title is a principle, not a claim that senior developers finish every week with a negative line count. New products need new code. A new integration, business rule or user interface cannot always emerge from subtraction. Measuring engineers by deleted lines would be as absurd as measuring them by lines written.
The useful idea is that experience changes the direction of attention. A less experienced developer is usually asked to make a new path work. An experienced developer is also expected to see whether the new path makes an old one unnecessary, whether two workflows can become one and whether the requirement can be satisfied without creating another permanent concept.
That distinction matters because line count is a weak proxy for both complexity and value. Deleting ten generated files may change nothing important. Removing one boolean flag can eliminate four combinations of state. Replacing two nearly identical workflows with one explicit policy may add a few lines locally while deleting an entire category of future bugs.
The objective is not the smallest repository. It is the smallest system that clearly supports the behavior the product actually promises.
Every line has a carrying cost
Code is an asset only while it helps deliver a needed capability. At the same time, every executable path carries recurring cost.
- Understanding: somebody must decide whether the path matters before changing adjacent behavior.
- Testing: its contract needs evidence, and that evidence must survive future changes.
- Security: inputs, permissions, dependencies and data movement expand the attack surface.
- Operations: the path can fail, consume resources, emit misleading signals or require recovery.
- Coordination: other code, documentation and people can begin to depend on it.
This cost remains even when a function is rarely called. In fact, rarely used code can be more expensive because the team has less current knowledge about it. A forgotten admin action, legacy webhook or optional configuration branch may receive little routine testing while retaining production privileges.
This is why technical debt is not limited to ugly code. Perfectly formatted, well-tested code can still be debt when the behavior no longer deserves to exist. The cleanest implementation of an obsolete requirement is still an obsolete implementation.
Research published in 2025 on code improvement practices at Meta gives this work concrete organizational weight. The authors found that more than 14% of changes were explicitly devoted to code improvement. They describe activity ranging from organic engineer-led cleanup to structured initiatives that delete accumulated dead code. Codebase health was not treated as spare-time polish; it was recurring engineering work.
Experience changes the questions asked before coding
Early in a career, implementation skill is the visible challenge. Can I make the framework do this? Can I design the class? Can I get the request through the queue? As those mechanics become familiar, the harder challenge appears: deciding which behavior the system should own at all.
An experienced developer is more likely to ask:
- Is this a new requirement or an old requirement described with new words?
- Can an existing state represent this case without adding another flag?
- Does the product still need the path this change is replacing?
- Can the provider, framework or database already enforce this invariant?
- What can be removed after migration, and who owns that removal?
- Will another engineer understand why both versions exist six months from now?
These questions are not resistance to shipping. They are how a team preserves its ability to ship the next change. Each unnecessary option increases the number of combinations that testing, operations and human reasoning must cover.
I see this most clearly in mature Laravel applications. Adding another service class is easy. Determining that two services encode the same business decision under different names is harder. Adding a feature flag takes minutes. Proving that the old branch can be removed after rollout requires telemetry, coordination and confidence. Creating another queue is straightforward. Simplifying ownership so that an existing workflow can handle the job requires architectural judgment.
The code editor is often the last tool used in this work, not the first.
What experienced developers actually delete
Safe deletion is broader than finding unused private methods. The best candidates usually sit at the boundaries between product history and current reality.
1. Dead behavior, not only dead functions
A static analyser can find some unreachable methods and unused imports. It cannot always tell whether a working endpoint supports a customer, whether an event still has a consumer or whether an old pricing rule is legally required. Behavioral deletion starts with usage evidence and ownership.
When a capability is genuinely retired, I want its full vertical slice removed: route, controller, policy, service, queue handler, configuration, tests, dashboards and documentation. Leaving the surrounding pieces “just in case” preserves ambiguity. Version control already remembers the implementation.
2. Transitional code after the transition
Migrations create temporary duality: old and new columns, two event formats, compatibility adapters, read fallbacks and feature-flag branches. Temporary code is reasonable when it makes a rollout reversible. It becomes permanent complexity when nobody defines the condition for removing it.
I prefer to attach an explicit deletion trigger when adding transitional code: all records migrated, old clients below a threshold, provider cutover verified or rollback window closed. A calendar reminder is weaker than a measurable condition, but it is better than an undocumented promise to clean up later.
3. Speculative abstractions
An abstraction built for one real case and three imagined cases often hides the only behavior that matters. Generic names such as Manager, Processor and Handler can make a codebase look flexible while moving domain decisions into configuration and indirection.
Martin Fowler's explanation of YAGNI draws an important boundary: the principle applies when supporting a future need adds complexity now. This is not an argument against design. It is an argument against paying a complexity cost before the requirement produces value or evidence.
When the predicted variation never arrived, deleting the extension points is not an admission of failure. It restores a design shaped by facts.
4. Duplicate sources of truth
Two caches, two status fields or two ways to calculate the same value force the team to reason about synchronization. Sometimes duplication is deliberate for performance or availability. Often it survives because removing one representation feels riskier than continuing to reconcile both.
Deletion can turn synchronization into derivation. If a value can be computed reliably from an authoritative record, removing the writable copy eliminates invalid combinations. A smaller data model is frequently more valuable than a smaller method.
5. Dependencies that exceed their value
A library can remove hundreds of lines while adding a large external contract. That trade can be excellent. It can also age badly when the application uses one small function but inherits release churn, transitive packages, security advisories and framework constraints.
Experienced developers do not automatically replace packages with custom code. They periodically reevaluate the boundary. Removing a dependency is worthwhile when the capability is no longer needed, the platform now provides it or the integration cost exceeds the implementation it replaced.
Deletion is harder than addition because absence needs proof
A new feature has an obvious demonstration: perform an action and observe the result. A deletion must establish that no important actor needs what disappeared. That is a negative claim across users, jobs, integrations, data and operating procedures.
This is why “I searched the repository and found no references” is useful but incomplete. Dynamic calls, scheduled tasks, external clients, copied URLs, manual support procedures and data exports may not appear as direct references. The older the system, the more likely its real contracts extend beyond the type system.
Deletion also carries emotional friction. The code may represent weeks of past effort. Its author may still be on the team. A complicated abstraction can become part of a developer's identity because they understand it better than anyone else. None of those factors prove current value, but ignoring them makes cleanup unnecessarily adversarial.
I frame deletion around the obligation, not the author: Which supported outcome depends on this? What evidence would make us confident? What rollback is available? The question is whether the system still needs the behavior, not whether writing it was a mistake.
A safe workflow for deleting production code
Confident deletion is evidence-driven. For anything beyond an obviously unused local symbol, I use a sequence like this.
- Name the candidate in product language. “Remove the legacy invoice download” is clearer than “delete
InvoiceExportController.” It defines the behavior being retired. - Map the complete slice. Find entry points, callers, data, permissions, background work, alerts, documentation and external consumers. Search code and configuration, but also inspect runtime evidence.
- Establish ownership. Confirm who can decide that the behavior is no longer supported. Technical silence is not product approval.
- Measure actual use. Add temporary logging or a metric if existing telemetry cannot answer the question. Choose a window that covers monthly jobs, renewals and other low-frequency behavior.
- Separate retirement from cleanup when risk requires it. First stop new use or return an explicit deprecation response. Delete storage and compatibility code after the rollback window closes.
- Make the change reviewable. Keep deletion separate from unrelated refactoring. Google’s engineering practices note that deleting an entire file is usually easy to review; mixing removal with a redesign hides the important contract change.
- Test what remains. The most important tests prove supported outcomes still work and removed states are no longer reachable. Delete tests that only preserve retired behavior.
- Observe after release. Watch error rates, not-found requests, queue failures, support signals and the business outcome the old path served.
- Finish the deletion. Remove flags, fallback reads, dashboards, runbooks, environment variables and stale documentation. Half-deleted behavior creates a new kind of uncertainty.
This process makes deletion reversible where possible. A staged retirement may feel slower than one large removal, but it converts unknown dependencies into observable evidence. That is the same logic I use when designing reversible failures: preserve a safe next action while uncertainty remains.
Deletion improves more than maintainability
| What disappears | Immediate effect | Long-term effect |
|---|---|---|
| An obsolete endpoint | Smaller attack surface | Fewer contracts to secure and document |
| A feature-flag branch | One production path | Fewer state combinations in every future change |
| A duplicate data field | No synchronization job | Fewer invalid data states |
| A speculative interface | Behavior becomes visible | Changes follow real requirements |
| An unused dependency | Smaller dependency graph | Less upgrade and vulnerability work |
| A legacy workflow | One operating procedure | Clearer ownership and recovery |
The compounding effect matters. A removed branch is not only absent today. It no longer participates in next year's framework upgrade, security review, incident investigation or onboarding session. Deletion pays a recurring dividend in attention.
This connects directly to the hidden cost of unexplained abstractions. A line can be syntactically simple while forcing every reader to reconstruct why it exists. Removing an unnecessary concept reduces cognitive load more effectively than adding a better comment around it.
AI makes deletion more important, not less
AI coding tools have reduced the cost of producing plausible implementation. They have not reduced the cost of owning it.
A model can generate adapters, validators, interfaces, tests and fallback branches in seconds. Each addition can look locally reasonable. The aggregate may still create three representations of one concept, defensive handling for impossible states and abstractions justified only by the code the model generated around them.
This changes the bottleneck. When implementation is abundant, restraint becomes more valuable. The experienced developer's job is to decide which generated paths deserve to become part of the product's permanent contract.
My review question for AI-generated code is not only “Does this pass?” It is “What new obligation does this introduce?” I look for duplicated validation, compatibility layers nobody requested, broad exception recovery that hides failure and helper classes used once. Then I ask the model—or myself—to remove them and prove the remaining behavior.
This is part of being AI-assisted rather than AI-dependent. Generation optimizes for a complete-looking answer. Engineering judgment optimizes for a system the team can continue to understand.
Questions to ask in the next code review
Code review naturally focuses on whether the addition works. Add a second pass focused on what the change makes unnecessary:
- Does this replace an existing path, and is that path removed in the same change?
- Is a new option representing a real requirement or a hypothetical future?
- Can an existing invariant remove the need for this validation branch?
- Does this migration have a measurable completion and deletion condition?
- Are we creating another source of truth?
- Could the framework or platform own this behavior safely?
- Which tests document valuable behavior, and which only protect old structure?
- What configuration, telemetry and documentation should disappear with the code?
- Can the diff be split so reviewers can see the deletion clearly?
These questions do not guarantee a smaller diff. Sometimes answering them reveals that the safe solution needs a migration, new telemetry or a temporary adapter. That is acceptable. The goal is to make temporary complexity explicit and give it an exit.
Delete decisions, not just lines
The most valuable deletion is often a decision removed from the path of the next developer. There is one status instead of two competing flags. One workflow instead of legacy and current modes. One source of truth instead of a synchronization rule. One clear failure instead of three speculative fallbacks.
That is why experienced developers appear to delete more code. They have seen how quickly small obligations accumulate and how slowly teams recover the attention those obligations consume.
Writing code proves that a solution can exist. Deleting code, safely, proves that the system no longer needs part of its past.
The best deletion does not make the product smaller. It makes the product's promises easier to see.
Sources and further reading
- Code Improvement Practices at Meta (2025)
- Google Testing Blog: Sensenmann - Code Deletion at Scale
- Google Engineering Practices: Small CLs
- Martin Fowler: YAGNI
Sources were checked on September 29, 2026. This article reflects practical experience from more than seven years of software engineering; examples describe general engineering patterns and do not identify an employer or client. It was prepared with AI assistance and editorial review. Cover: laptop and code photograph by StockSnap on Pixabay.
