The Complexity We Choose
The current premise of software development is token maximization. More tokens mean more code, more generated output, and more features shipped in less time. The assumption is that if an AI system can produce ten times as much code, we will be ten times as productive.
That assumption ignores the bill that arrives later.
Code is not valuable because it exists. It is valuable because people can understand it, change it, operate it, and trust it. When code becomes harder to read than the problem it solves, every future change becomes more expensive. The team spends its time reconstructing intent instead of delivering value.
We have spent decades treating this complexity as an unavoidable cost of software development. It is not always unavoidable. Often, it is simply the accumulated result of choices nobody had time to revisit.
The token ceiling is not the real ceiling
Large language models have made the cost of producing code extremely low. A developer can describe a feature and receive a plausible implementation in seconds. An agent can inspect a repository, modify several files, and propose a pull request before a human has finished opening the editor.
This is powerful, but it changes the shape of the problem. The scarce resource is no longer only typing. It is comprehension.
Generated code still has to be read. Its assumptions have to be checked. Its failure modes have to be understood. Someone has to know whether a change belongs in the domain layer, the adapter layer, or nowhere at all. Someone has to debug it at two in the morning.
If we use AI only to generate more code, we are optimizing the cheap part of the work while making the expensive part larger. The repository gets bigger, the dependency graph gets denser, and the number of possible interactions grows. A system can compile and pass a narrow test suite while becoming progressively less intelligible.
More tokens are not the same as more understanding.
Who owns software written by a model?
There is another question hiding beneath the productivity story: ownership.
If an AI system writes a company’s internal software, who owns that software? The practical answer is usually determined by employment agreements, vendor contracts, applicable copyright law, and the amount of human authorship involved. A model provider does not automatically become the owner of every program generated by its model.
But legal ownership is only one part of the question. A company can own the repository and still lose effective ownership of the system if nobody inside the company can explain it. If the organization cannot safely modify, replace, or operate an internal tool without depending on a particular model or vendor, it has control on paper but not in practice.
This is the more important form of ownership: the ability to understand and change what you depend on.
Model providers may influence the software indirectly. They control the model, its updates, its availability, its usage terms, and sometimes the surrounding tools that make an agent effective. A team that accepts generated code without preserving its rationale, tests, and architectural boundaries can become dependent on a system it does not own.
The answer is not to reject AI. It is to make the output comprehensible and portable. A company should be able to explain its software without referring to the private reasoning of a model. It should be able to change models without rewriting the system. It should own the decisions, the tests, the operational knowledge, and the constraints that make the code safe.
Complexity is an AI problem too
The most under-discussed opportunity in generative AI is not generating software. It is reducing software complexity.
An AI system performs better when it has less irrelevant context to process. A small module with explicit boundaries is easier for a model to reason about than a huge service where business rules, persistence, networking, and formatting are mixed together. Clear names are better prompts than comments that describe accidental behavior. A narrow interface is a better contract than a convention spread across twenty files.
Software that reads like a book helps both humans and machines. It has a beginning, a clear sequence of decisions, and an understandable conclusion. Its abstractions earn their existence. Its data flow can be followed without holding the whole repository in working memory.
This is not an argument for simplistic software. Complex domains will remain complex. The goal is not to pretend that a difficult business rule is easy. The goal is to keep accidental complexity from obscuring essential complexity.
AI can help with this work:
- identify duplicated logic and conflicting rules;
- map dependencies and reveal unnecessary coupling;
- find dead code, obsolete abstractions, and unused configuration;
- propose smaller modules and safer boundaries;
- summarize behavior before a refactor;
- generate characterization tests for legacy code;
- compare the implementation with the documented domain rules;
- detect when a new feature is adding a second way to do something the system already knows how to do.
These are high-value uses because they reduce the amount of context required for every future change. A refactor is not merely an aesthetic improvement. It is a reduction in the cost of every human and AI interaction that follows.
Be brave with system-wide refactors
Most teams know where their software is painful. There is a class with too many responsibilities, an API with contradictory conventions, a workflow that can only be changed by one person, or a service whose behavior is distributed across configuration and side effects.
Still, teams often avoid system-wide refactoring because it feels too risky. The result is a series of local patches that preserve the original problem and add new exceptions around it.
The answer is not to refactor recklessly. It is to refactor with evidence.
Start by describing the behavior that must not change. Capture it with tests, production observations, contract checks, or carefully selected examples. Then establish a seam, move one responsibility at a time, and keep the system runnable throughout the transition. Use the tools that make change visible: dependency graphs, compiler errors, static analysis, metrics, and review.
A broad refactor can be safer than endless local work when the existing design is the source of the risk. The boundary that is painful to cross today will not become easier because we have added another feature behind it.
We should be especially willing to simplify code that is frequently changed. Complexity in stable code is sometimes tolerable. Complexity in a central, evolving path is a tax charged on every release.
Guard the software with tests
Readable code is a goal. Feedback is how we get there safely.
Unit tests protect local rules and make small design changes inexpensive. Integration and contract tests protect the boundaries between components. Gherkin scenarios can express important business behavior in language that product, QA, and engineering can review together. Exploratory QA catches surprising interactions that no formal suite anticipated.
Quality metrics provide another perspective. Test coverage can reveal unexamined code, although a high percentage alone does not prove useful behavior. Mutation testing asks a harder question: if the implementation is deliberately changed, do the tests notice? Static analysis can enforce architectural rules. Performance, reliability, accessibility, and security checks can become part of the definition of done instead of an emergency phase at the end.
None of these tools is sufficient by itself. Together, they create a safety net that lets engineers improve structure without guessing whether they broke behavior.
This safety net matters even more when AI is writing code. A model can generate a confident implementation that is locally plausible and globally wrong. Tests turn the repository into an executable specification. They give an agent a way to learn the system’s boundaries without consuming the entire codebase as context.
The strongest AI-assisted workflow is therefore not:
Ask for code, accept code, repeat.
It is:
Describe intent, make behavior observable, simplify the design, generate a focused change, and let independent checks challenge it.
A different definition of productivity
We should measure more than lines changed, tokens consumed, or tickets closed. Useful signals include:
- how long it takes a new engineer to make a safe change;
- how much of the system a developer must understand to modify one rule;
- how often changes require unrelated edits;
- how quickly a failed deployment can be diagnosed;
- how much duplicated behavior exists;
- whether tests detect meaningful regressions;
- whether the team can replace a model or vendor without losing its engineering knowledge.
These measures reward software that remains legible. They also reward engineering work that is easy to postpone and expensive to ignore.
The future may contain much more AI-generated code. That future does not require us to accept much more complexity. In fact, it gives us a reason to demand less. If machines can produce code quickly, humans can spend more of their time deciding what should exist, simplifying what already exists, and building the constraints that keep both humans and machines honest.
The prize is not the largest codebase we can generate. It is software that can be read like a book, verified like a claim, and changed without fear.
We finally have tools that can help us reach that standard. We should use them to make software smaller in spirit, even when it becomes larger in capability.
Enjoy Reading This Article?
Here are some more articles you might like to read next: