Less is More

As AI makes code abundant, the harder engineering judgment is deciding which code deserves to remain.

Pluralitas non est ponenda sine necessitate.

Plurality should not be introduced without necessity.

This formulation of Occam’s razor was written for philosophy, not software. Yet it increasingly feels like the principle I need most when writing and reviewing code with AI.

For most of software history, producing code was expensive. Every new feature required someone to think through the logic, type the implementation, debug it, and explain it to another person. The cost of creation imposed a natural restraint.

LLMs have removed much of that friction.

Ask for a small feature, and the model may generate the component, helper functions, validation layer, error handling, configuration options, defaults, tests, comments, abstractions, and support for several hypothetical future cases. On first impression, the result looks impressive. It appears comprehensive and carefully engineered.

Then I look more closely.

There are conditions for situations that cannot occur. Defaults no one requested. Fallbacks that conceal invalid states instead of preventing them. A reusable abstraction used only once. A dependency introduced to avoid writing ten straightforward lines. The code works, but the solution is larger than the problem.

The model has completed the task. It has not necessarily understood what the task should exclude.

A Codebase That Only Grows

GitClear’s 2026 analysis of software changes describes what it calls a growing “maintainability gap.” Its report argues that current AI workflows are optimized for atomic completion — a passing test, a working feature, a closed ticket — while underweighting consolidation and long-term ownership. (GitClear)

That description matches my experience.

The agent is usually solving the visible request in front of it. It does not naturally feel responsible for the total conceptual weight of the repository. Adding a new helper may be safer than modifying an existing one. Creating a new component may require less contextual reasoning than understanding why the current component already exists. Handling an invalid state may be easier than redesigning the model so the state becomes impossible.

Armin Ronacher recently described present-day coding models as too defensive, too complex, and too local in their reasoning. Instead of enforcing strong invariants, they tend to add fallbacks and local protections. Put that behavior inside an automated loop, he argues, and every iteration can add another small defense until the system becomes less understandable while appearing more robust. (Armin Ronacher’s Thoughts and Writings)

This is the paradox of AI-generated software: it can look increasingly complete while becoming progressively harder to comprehend.

The Work After Generation

AI can build the feature, satisfy the tests, and close the ticket. That is useful. It is not the same as engineering a system worth maintaining.

The harder work is deciding what should remain.

What can be removed? Which checks represent real requirements? Can two paths become one? Should an invalid state be handled, or made impossible? Does an abstraction simplify the system, or add another concept?

Adding code needs only local context. Removing a fallback or consolidating two implementations requires understanding the system’s invariants and architecture.

This is why output is a poor measure of AI productivity. Generating faster is not finishing faster. Reviewing, correcting, and integrating generated work is consuming the apparent time savings. More code is not always a better system.

The Philosophy: Pluralitas Non Est Ponenda Sine Necessitate

This is where Occam’s razor becomes my guiding philosophy for coding while working with AI.

AI-generated code can multiply entities, functions, classes, dependencies, and abstractions, beyond what the problem requires. The output may look complete while its added complexity makes the codebase harder to understand and maintain.

“Less is more” is not about writing less code for its own sake. It is about intentionality. Every line should justify its existence. Every abstraction should earn its keep. Every dependency should serve a real need.

This is where LLMs need the most direction. They readily generate speculative features, defensive checks, and unnecessary wrappers before the requirements are fully understood. The engineer’s job is to decide what belongs in the system and what does not.

Reviewing for What Can Be Removed

When a small request arrives as a large generated diff, I do not begin by reading it line by line. I first restate the requested behavior in concrete terms. That becomes the filter: which files, branches, configuration, and dependencies are actually needed to deliver it?

Then I trace the main path through the change. I want to see where data enters, how it is transformed, and what result reaches the user. Code that does not support that path needs a reason to exist. A new helper, option, fallback, abstraction, or dependency should answer a simple question: what real requirement makes this necessary?

Passing tests are only the first checkpoint. They show that the code can work; they do not show that every branch, default, or layer deserves to stay. Once the change is correct, I make a separate reduction pass: remove unused paths, merge duplicated logic, discard speculative options, and replace local defenses with a stronger invariant when possible.

This is not a demand for the fewest lines. Clever compression can hide the domain just as easily as generated bloat can obscure it. A longer implementation is better when it makes the system clearer.

The goal is necessary code: everything required to express the system clearly, and nothing kept merely because generation was easy.

LLMs have made code abundant. The engineer’s role is no longer only to produce an implementation. It is to decide what deserves to remain.

Pluralitas non est ponenda sine necessitate.

Do not multiply what the system does not need.