03.04Overview
Design principles for AI-generated code
Design principles AR Labs applies to code from AI coding agents: build only what was asked, deep modules, two cases before a pattern, one source of truth.
AR Labs holds code written by AI coding agents (AI agents that write code, such as Claude Code or Codex) to classical design principles, and pays most attention to the ones agents break by default. Agents over-generalise, reach for patterns they have seen many times, duplicate what already exists, and leave comments that point at documents nobody will open again. The principles below are our standing answers, and the architecture axis of every cross-model review checks against them.
The problem it answers
Code written by an agent can be correct line by line and still leave a codebase that is harder to change than it should be. No single piece looks wrong; the cost is in the accumulation. That cost then compounds, because the next agent works in the codebase the last one left. Matt Pocock puts it plainly in his public workshop on a workflow for AI coding: bad codebases make bad agents. Codebase quality is an input to agent quality, not only an output of it.
The principles
flowchart TD
Q1{"Was it asked for?"} -->|"no"| X["Do not build it"]
Q1 -->|"yes"| Q2{"Does it already exist?"}
Q2 -->|"yes"| U["Reuse it"]
Q2 -->|"no"| Q3{"Two real cases for a pattern?"}
Q3 -->|"no"| S["Write the simple version"]
Q3 -->|"yes"| P["Apply the pattern"]
S --> D["Small interface, deep body"]
P --> DBuild only what was asked. Every method, parameter and branch is a standing cost: someone, person or agent, has to read it, test it and hold it in context for as long as it exists. The imagined benefit arrives only if the need ever does. The test at each decision is whether the capability is in the spec, the acceptance criteria or an explicit request. If not, it waits until it is. This is YAGNI ("you aren't gonna need it") from Extreme Programming, together with KISS ("keep it simple"), and it is the principle that most directly resists the debt agents introduce.
Deep modules over shallow ones. From John Ousterhout's A Philosophy of Software Design: a module should offer a small, simple interface over substantial behaviour. Shallow modules force every caller to understand their internals, multiply dependencies and make tests brittle. They also make agents worse, because an agent must hold the whole dependency graph in context to make one change. We design the interface and delegate the body. A small interface is the narrow-interface half of the Interface Segregation Principle, applied as a habit, and the Facade pattern applied everywhere rather than once.
Two concrete cases before a pattern. Agents apply familiar patterns early because those patterns appear so often in their training data. Our test before applying one: are there two concrete implementations now? If yes, apply it. If a second is certain within the current piece of work, apply it and note the assumption. If the second is speculative, write the simple version and refactor when the second case arrives. Three similar lines of code that do slightly different things are three lines of code, not a design flaw.
| Agents tend to over-apply | Symptom | Corrective |
|---|---|---|
| Strategy | Interface, one implementation and a factory for something that does one thing | Use a function until a second strategy exists |
| Factory | A factory that creates exactly one type | Use a constructor |
| Singleton | Global state behind a static accessor | Create one instance and pass it explicitly |
| Observer | An event system between two objects that could call each other | A direct call, until there are several subscribers |
| Agents tend to under-apply | What is missed |
|---|---|
| Adapter | External APIs called directly, so one API change ripples through the codebase |
| Facade | Subsystems exposed in full, so every caller must learn the internals |
| Command | Operations that should be retryable or undoable are inlined |
| State | Mode-dependent behaviour buried in nested conditionals |
One source of truth. Each fact, rule or piece of data lives in one place, and everything else refers to it. Duplication creates ambiguity about which copy is right and errors when they drift. The same rule governs our documents, and our agents search for existing code before writing new code (see reuse before create).
Separation of concerns, tested by change. Each part has one reason to exist and one reason to change. The test: if one external provider were swapped, which parts would change? One means the concerns are well separated.
Comments state the design, not its history. A comment must make sense to a reader who has never opened the task tracker or the spec. It says what the code does and why in the language of the domain, and if it records where a decision came from, the reference goes in brackets at the end, where deleting it costs no meaning. No changelogs in comments, since git already holds the history. No decision codes borrowed from a spec ("guard B", "decision D4"), because specs are archived when the work closes and a comment that indexes into one becomes a dead symbol.
Feedback loops set the ceiling. An agent's output cannot be better than the feedback available to it. Type checks, unit tests, integration tests and linting each raise the ceiling; a suite that only checks compilation lowers it. When output is poor, we check the feedback loops before blaming the model. This is why test-first development is mandatory for agents.
Size work for a fresh context. Matt Pocock describes a "smart zone" early in a context window and a "dumb zone" later, where reasoning degrades; his own working marker is around 100,000 tokens, whatever the advertised window. We size each task to finish well inside a fresh session and start the next task from written artefacts rather than from a compacted conversation.
A worked example
From our own code, with names simplified. A gate evaluator in a voice agent judged whether a stage of the conversation was complete. It grew a second method beside evaluate(), called evaluate_with_usage(), which returned the same result plus token counts as a tuple, and the same doubling happened on the closing path: four methods where two were needed. Nobody had asked for token accounting. It was added to gather data for a decision the spec had deliberately deferred, with an explicit instruction not to optimise for it in advance.
The cost was permanent: twice the interface, two return shapes, and a question on every read about which variant to call. Under "build only what was asked", the simple version is one evaluate() whose result object can carry usage figures when they exist; callers that do not care never look at them. We keep this case as the worked example of what the rule forbids.
Where it came from
The principles draw on Ousterhout's deep modules, Robert C. Martin's SOLID principles, the Gang of Four's design patterns catalogue, Extreme Programming's YAGNI and Matt Pocock's workflow for AI coding. The tables of patterns agents over-apply and under-apply are our own observations from reviewing agent-written code.