Software systems rarely become difficult to maintain because of one spectacularly bad decision. Complexity more often accumulates through individually defensible choices: keep the old interface, add a compatibility wrapper, preserve the previous route, mark it deprecated, and move forward without actually removing anything.
The immediate change feels safer because nothing disappears. The long-term system becomes less safe because everything remains possible.
Recent large-scale Google research across more than 1,200 C++ and Java projects found that greater architectural complexity and structural anti-patterns were associated with more code being spent on bug fixing rather than feature work. Earlier empirical research also found that tightly coupled components cost more to maintain across very different software systems.
The wrapper is not the problem. Permanent wrappers are.
Abstraction is fundamental to software engineering. A good abstraction hides implementation detail, creates a stable boundary, or lets components change independently. The failure mode begins when an abstraction exists mainly because the previous abstraction was never changed or removed.
At that point the runtime is carrying its own history as topology. Every additional layer has to preserve assumptions from the layers beneath it, so debugging becomes an archaeological exercise.
There are legitimate migration patterns that introduce temporary abstraction. Martin Fowler's Branch by Abstraction is explicitly designed for gradual large-scale change, and the broader Transitional Architecture pattern treats migration components like scaffolding: useful while the replacement is happening, but intended to be removed when the target architecture is reached.
The mistake is not building scaffolding. The mistake is moving into the building and deciding the scaffolding is now a permanent feature.
The open door problem
Soft deprecation creates a different kind of architectural dishonesty. Imagine a door with a large sign that says RETIRED — ENTRY FALSE — DO NOT USE. The sign is clear. The door is also unlocked and standing open.
From the building's perspective, the room is still accessible.
Software does this constantly. An endpoint is marked deprecated. A route is declared noncanonical. A configuration field says it is disabled. Documentation tells callers to use the replacement. But the executable path still exists and still works.
Semantic Versioning treats deprecation as a transition rather than an endpoint: users are warned in a minor release, and incompatible removal belongs in a later major release. Google's engineering guidance similarly notes that when code is deleted or deprecated, associated documentation should be considered for deletion or update as part of the same change.
A sign is useful while consumers migrate. A sign is not enforcement. Once the system knows a path should never again be selected, the architecture should eventually make that path impossible.
Feature flags show the same carrying cost
Feature flags are another legitimate form of temporary parallel behavior. They let teams ship safely, stage releases, and separate deployment from exposure. But they also add branches, configuration states, and testing combinations. Fowler's feature-toggle guidance describes this as a carrying cost and recommends proactively removing flags that are no longer needed; some teams even give flags expiration dates or tests that fail when a flag overstays its intended lifetime.
The same accounting should apply to wrappers, aliases, adapters, fallback compilers, compatibility endpoints, and legacy routes. If a temporary mechanism remains in production, it should be able to explain why it still pays rent.
Versioning and wrapping solve different problems
Versioning records evolution. Wrapping preserves simultaneous implementations. Those are different jobs.
If an internal component reaches version 4 and its design is wrong, one option is to leave version 4 untouched and surround it with another compatibility layer. Another is to make version 5 the corrected canonical implementation and remove the obsolete execution path once migration is complete.
Source control already preserves history. Production architecture does not need to become a museum of every implementation the system has ever used.
AI agents turn dead code into action-space entropy
This problem becomes more important when software is operated by AI agents. A conventional program follows routes selected explicitly in code. An agent may choose from tools, schemas, route names, repository instructions, historical context, and runtime responses.
That means an obsolete but discoverable path is not merely maintenance debt. It is another candidate action.
If documentation says that three of those are retired, the model still has to reason correctly about four visible choices. If only FILE_READ exists, the incorrect decision disappears from the model's action space.
This extends the architecture described in our earlier research on separating models from execution runtimes and preserving durable state outside any one model. The same separation only works reliably when the runtime itself converges on one canonical path instead of exposing its entire historical vocabulary.
Google Research's 2026 discussion of technical debt in the AI era argues that AI changes how software is created without eliminating the need to manage technical debt. Our additional thesis is that agentic systems add a new form of cost: action-space entropy. Historical execution paths are not only code to maintain; they are decisions autonomous systems can rediscover.
Compatibility should have a named consumer
There are good reasons not to delete a route immediately. A public API may have outside customers. Old mobile clients may still be deployed. A database migration may require dual reads. A distributed rollout may need two schemas to coexist.
Those are compatibility requirements because they name an actual dependency.
"Keep it just in case" is different. A compatibility layer without a known consumer, an observable migration, and a deletion condition is often simply undeleted legacy architecture.
- Modify the canonical implementation before creating another wrapper.
- Use wrappers only for real architectural boundaries or bounded migrations.
- Give every temporary compatibility layer a deletion condition when it is created.
- Treat deprecation as a transition, not a permanent resting state.
- Once a path is deterministically invalid, make it unreachable.
- Preserve history in version control rather than production topology.
- Require evidence of a real consumer before preserving compatibility.
- Regression-test that the losing path is impossible or ineffective after migration.
- For AI-operated systems, minimize obsolete or equivalent actions exposed to the model.
- Prefer one server-enforced canonical path over several paths distinguished only by instructions.
The strongest architecture does not ask permission from memory
Documentation matters. Naming matters. Deprecation warnings matter during migration. But the mature endpoint is structural.
If only one route is correct, the system should not depend on every future engineer, model, script, or integration remembering which sign on which door said not to enter.
A replacement should replace. A migration should finish. A temporary wrapper should expire. A deprecated path should approach deletion.
And when the system knows that a door must never be used again, the final fix is not a better sign.
The door should be closed.
Lensically Research
We publish the engineering lessons that fall out of building AI-operated systems in production, including the mistakes that force the architecture to get better.
Explore Lensically Research