Skip to content

Keeping AI-Generated Software Understandable

AI can turn a short request into working code very quickly. That does not mean the resulting system is simple, understandable, or safe to operate.

For a beginner, the distinction is:

  • working code produces the expected result in the cases that were tried
  • understandable software has responsibilities, dependencies, decisions, and failure behavior that people can explain
  • maintainable software can be changed without repeatedly breaking unrelated behavior

Agent-generated code is not automatically worse than human-written code. The risk comes from speed and scale. An agent can produce more changes than a person can meaningfully inspect, and a sequence of individually reasonable changes can create an unreasonable architecture.

The goal is therefore not to avoid AI-generated code. It is to use AI for implementation while preserving human understanding of the system.

Software development has never been only about translating an idea into programming-language syntax.

The phrase software crisis emerged around the 1968 NATO Software Engineering Conference, when software projects were becoming harder to deliver and control as computers and ambitions grew. In his 1972 Turing Award lecture, Edsger Dijkstra argued that increasingly powerful machines had created a new difficulty: programmers now had to master systems whose complexity could exceed their intellectual control.

The historical analogy is useful, but it is not a prediction that every AI-assisted project will fail. Coding agents create another large increase in production capacity. If a team can generate software faster than it can understand and simplify it, complexity can once again outrun the practices used to manage it.

In No Silver Bullet, Fred Brooks separated software difficulty into two broad categories:

  • essential difficulty comes from understanding and representing the problem itself
  • accidental difficulty comes from the tools and mechanisms used to implement that solution

Better languages, libraries, infrastructure, and coding agents can remove substantial accidental work. They do not automatically decide:

  • what the system should do
  • which behavior is truly required
  • where responsibilities should live
  • which tradeoffs are acceptable
  • how the system should fail
  • what must remain compatible

AI makes implementation easier, but the conceptual work remains. In some projects, implementation becomes fast enough that understanding, review, and architecture become the limiting resources.

flowchart LR
    I["Intent and domain knowledge"] --> D["Design decisions"]
    D --> C["Code generation"]
    C --> V["Verification"]
    V --> O["Operable software"]
    A["Coding agent"] -. "Accelerates" .-> C
    H["Human owner"] -. "Applies judgment" .-> D
    E["Tests and runtime evidence"] -. "Ground" .-> V

Rich Hickey’s Simple Made Easy distinguishes two ideas that are often treated as synonyms.

IdeaPractical meaningExample
EasyClose at hand, familiar, or requiring little immediate effortAsk an agent to add one more condition to the existing function
SimpleNot interleaved or entangled with unrelated concernsKeep authorization, business rules, persistence, and presentation behind clear boundaries

An easy action can make a system less simple.

Suppose an application has one authentication file. A sequence of prompts asks an agent to:

  1. add password login
  2. add OAuth
  3. add session refresh
  4. add enterprise SSO
  5. repair a conflict between the previous changes

Each response may be locally reasonable. If every request modifies the nearest working path, however, authentication, session storage, user provisioning, and authorization can become intertwined.

flowchart TD
    A["Prompt 1<br/>Add authentication"] --> B["Small local solution"]
    B --> C["Prompt 2<br/>Also add OAuth"]
    C --> D["Extend the existing path"]
    D --> E["Prompt 3<br/>Sessions now conflict"]
    E --> F["Add another exception"]
    F --> G["More coupling and hidden assumptions"]
    G --> H["System works, but its structure<br/>is difficult to explain"]

This is a conversation spiral: the architecture begins to reflect the order and wording of the conversation instead of a deliberate model of the problem.

The distinction is useful when an agent reads an existing codebase.

ComplexityWhat it representsExample
EssentialRules that exist because of the real problemOnly an account owner may approve a transfer
AccidentalDifficulty created by implementation historyThe same ownership check is copied into controllers, jobs, and database helpers

An agent sees code, tests, documentation, history, and tool output. It does not automatically know which patterns express current intent and which are old workarounds.

For example, repeated permission checks may mean:

  • every layer genuinely needs an independent security boundary
  • the application lacks a central authorization service
  • a previous migration stopped halfway
  • an old compatibility rule is still required for one customer
  • obsolete code was copied because it appeared nearby

Similarity alone cannot resolve that question. The agent needs evidence from domain owners, architecture decisions, production behavior, history, and tests.

This is why “follow existing patterns” is necessary but insufficient. Existing patterns are evidence, not proof that the design should be repeated.

Why Passing Tests Does Not Guarantee Understanding

Section titled “Why Passing Tests Does Not Guarantee Understanding”

Tests are essential, but they answer bounded questions.

A passing suite can show that:

  • specified examples still work
  • known invariants were preserved
  • interfaces remain compatible
  • a regression was not reproduced

It may not show that:

  • responsibilities are in the correct component
  • two concepts have become unnecessarily coupled
  • a copied workaround is now spreading
  • an undocumented behavior should have existed at all
  • the design will remain comprehensible after the next five changes

Verification and architectural review therefore solve different problems. Use both.

Signs That Understanding Is Falling Behind

Section titled “Signs That Understanding Is Falling Behind”

Slow down when several of these appear:

  • reviewers can describe the diff but not the resulting execution path
  • the agent repeatedly edits the same central file for unrelated features
  • each new request adds another exception to the previous solution
  • generated abstractions duplicate an existing responsibility
  • a change passes tests but nobody can explain why the affected boundaries are correct
  • the plan names files without explaining their roles
  • the agent cannot distinguish required behavior from compatibility scaffolding
  • generated documentation describes an architecture that the code does not actually implement
  • rollback depends on reconstructing decisions from a long chat transcript

The signal is not merely a large diff. A mechanical large diff can be easy to understand. A small change that mixes two previously separate concepts can be much riskier.

Before substantial implementation, a responsible person should be able to explain:

  1. Outcome: What problem are we solving?
  2. Essential behavior: Which domain rules must remain true?
  3. Boundaries: Which component owns each decision and side effect?
  4. Dependencies: What calls, stores, or trusts what?
  5. Failure behavior: What happens when a dependency is slow, unavailable, or inconsistent?
  6. Compatibility: Which current consumers and data must continue to work?
  7. Evidence: What will prove that the change is correct?
  8. Recovery: How can the change be rolled back or repaired?

The person does not need to memorize every line. They need a coherent model that lets them notice when generated code contradicts the intended design.

Current evidence supports this division of work. Anthropic’s June 2026 analysis of approximately 400,000 Claude Code sessions found that people made most planning decisions while the agent made most execution decisions. It also found that task-specific domain expertise was associated with higher success and better recovery from problems. The study is observational and uses model-based classification, so it should not be treated as universal proof, but it reinforces a practical rule: agents amplify the quality of the understanding supplied to them.

Use Research, Planning, And Implementation As Separate Phases

Section titled “Use Research, Planning, And Implementation As Separate Phases”

For consequential work, do not begin with an editing prompt.

flowchart TD
    G["Goal and constraints"] --> R["Research<br/>map current behavior and dependencies"]
    R --> RH{"Human checkpoint<br/>Is the system map accurate?"}
    RH -->|"Revise"| R
    RH -->|"Approve"| P["Planning<br/>design the change in detail"]
    P --> PH{"Human checkpoint<br/>Are boundaries and tradeoffs sound?"}
    PH -->|"Revise"| P
    PH -->|"Approve"| I["Implementation<br/>execute bounded steps"]
    I --> V["Verification and review"]
    V --> U{"Can a responsible person<br/>explain the resulting system?"}
    U -->|"No"| R
    U -->|"Yes"| M["Merge or release"]

The research phase should reconstruct reality before proposing changes.

Useful evidence includes:

  • relevant entry points and execution paths
  • interfaces, schemas, and type definitions
  • architecture and design documents
  • tests and production runbooks
  • recent incidents and known failure modes
  • git history for confusing decisions
  • migration notes and compatibility requirements
  • questions that require domain or architectural judgment

The output should be a bounded research document, not a dump of every file the agent read. It should separate:

  • confirmed facts
  • reasonable inferences
  • unresolved questions
  • suspected accidental complexity

This is context compression: turn a large, noisy codebase into a small, reviewable model of the part that matters.

Once the current system is understood, define:

  • the target behavior
  • component and ownership boundaries
  • exact interfaces that change
  • invariants that must be preserved
  • files and symbols likely to be modified
  • rollout and compatibility stages
  • tests, operational checks, and rollback
  • explicit non-goals

The plan should be precise enough that implementation is mostly execution rather than continuing architectural discovery.

Use Spec-Driven Development with Coding Agents when requirements, design, tasks, and evidence need durable traceability. Use Plan Before You Code for a lighter checkpoint.

Give the agent one reviewable step at a time or a dependency-aware set of independent steps.

For each step, specify:

  • allowed scope
  • required behavior
  • constraints and non-goals
  • verification commands
  • conditions that require stopping

If implementation reveals that the research or design is wrong, return to the earlier artifact. Do not hide the discovery under another local patch.

Create One Reference Change Before Automating A Migration

Section titled “Create One Reference Change Before Automating A Migration”

For a repetitive refactor or migration, manually completing one representative change can be more valuable than immediately dispatching many agents.

The reference change can reveal:

  • undocumented invariants
  • misleading abstractions
  • special compatibility cases
  • the actual review standard
  • commands and tests that prove correctness
  • differences between the intended architecture and the current code

The accepted pull request, its review discussion, and its verification evidence then become a concrete example for later agents.

This does not mean every migration must begin manually. Use a reference change when hidden knowledge or architectural judgment is more likely to dominate than mechanical editing. The complete procedure is covered in Parallel Coding Agents for Large Refactors.

Do not ask only, “What files did you change?”

Ask the agent to explain:

Describe the implemented execution path from input to side effect and response.
For each affected component, state:
- its responsibility
- why the change belongs there
- the invariants it preserves
- dependencies it calls
- expected failure behavior
- evidence that supports these claims
Identify any behavior copied from the existing code that may be compatibility
scaffolding or technical debt rather than intentional design.

Then compare the explanation with the code and runtime evidence.

A fluent explanation is not proof. Its value is that discrepancies become visible. If the explanation names a boundary that the code violates, or claims an invariant without a supporting test or source, the review has found useful work.

Not every change requires a research report and architecture review.

ChangeAppropriate process
Typo or isolated documentation correctionDirect edit and rendered check
Small behavior change in a familiar componentShort plan, focused tests, normal review
Cross-component featureSystem map, reviewed plan, integration tests, teach-back
Security, billing, identity, or data changeExplicit invariants, threat and failure analysis, human approval, rollback evidence
Repository-wide migrationReference change, dependency map, staged execution, layered verification

The cost of the process should follow the cost of being wrong and the difficulty of recovering understanding later.

Responsibilities vary by team and risk, but this is a useful starting point.

WorkHuman responsibilityAgent contribution
Problem definitionOwn outcome, priorities, and domain truthClarify ambiguity and summarize evidence
ResearchValidate important facts and answer judgment questionsSearch, trace paths, and produce a system map
ArchitectureOwn boundaries, tradeoffs, and risk acceptanceCompare options and test assumptions
ImplementationSet scope and stopping conditionsProduce code and mechanical transformations
VerificationDecide what evidence is sufficientRun checks and collect reproducible results
ReviewAccept accountability for the resulting systemExplain the change and perform independent checks

Avoid turning this table into a rigid rule. Agents can propose architecture and people can implement code. The important point is that consequential decisions have an identifiable owner who understands their effects.

The research document is plausible but imaginary

Section titled “The research document is plausible but imaginary”

Require file paths, symbols, test names, command output, or approved documentation for important claims.

Ask which existing behaviors are essential requirements and which are only implementation history. Escalate uncertain cases to domain owners.

An approval button is not review. Require the reviewer to resolve open questions and confirm boundaries, invariants, and verification.

The agent rewrites the plan while implementing

Section titled “The agent rewrites the plan while implementing”

Allow discoveries, but make them visible. Pause, update the reviewed artifact, and obtain approval when scope or architecture changes materially.

The reference change is not representative

Section titled “The reference change is not representative”

Choose a slice that exercises real interfaces and failure paths. If later batches differ materially, create another reference class instead of forcing one pattern everywhere.

Understanding exists only in one person’s head

Section titled “Understanding exists only in one person’s head”

Record the accepted system map, decisions, and operational evidence in repository documentation, design records, tests, or runbooks.

Before substantial generation:

  • The outcome and non-goals are explicit.
  • Essential behavior is separated from suspected implementation debt.
  • Current execution paths and dependencies are mapped.
  • Important claims point to evidence.
  • Human judgment questions are resolved or recorded.
  • The proposed boundaries and tradeoffs were reviewed.
  • Verification and recovery are defined.

Before accepting the result:

  • The complete diff matches the approved scope.
  • A reviewer can trace the changed execution paths.
  • The code follows the intended component boundaries.
  • Tests cover required behavior and important invariants.
  • Known accidental complexity was not copied without justification.
  • Operational failure and rollback behavior remain understood.
  • The resulting system can be explained without reconstructing the chat.
  • Faster code generation moves pressure toward understanding, design, and review.
  • Easy changes can produce a structurally complicated system.
  • Existing code contains both intentional design and historical accidents.
  • Tests verify bounded behavior; they do not replace architectural understanding.
  • Use research and planning artifacts to compress evidence before implementation.
  • Preserve human ownership of outcomes, boundaries, tradeoffs, and risk.
  • For uncertain migrations, solve one representative case before scaling execution.
  • Accept generated code only when the resulting system remains explainable and operable.

Diagram viewer