Reading code in the era of generated code
Plausible-reading code is often subtly wrong. Develop the new instinct.
I reject most of the LLM diffs that land in my review queue, and the reason barely changes from one to the next: the code is too functional, or it does not scale.
The diff compiles and the tests pass. The function works today and will keep working tomorrow, and it still will not survive the third feature that needs the same logic, because nothing was abstracted.
The advice on reading generated code shows up as checklists now: requirement fidelity, edge cases, API integrity, security, secrets, and every new tool ships a longer one. The assumption underneath is that if you check enough items, you have read the diff. The diffs I reject pass the checklists, because a checklist can only fail what is present in the diff, and what is wrong with these diffs is what is absent from them.
Peter Naur called a program "the shared theory in the heads of its authors," and generated code has no shared theory behind it. Reading it means building the theory yourself, from the diff. You used to read a diff to figure out what your colleague meant, and now you read one to ask whether the diff means anything at all.
I have worked across a lot of stacks in my career: C#, then vanilla JS, then Angular, Android in Java and later Kotlin, React, Python on Flask, Node, Java on the backend, Next.js. Each new one took less time to pick up than the one before, which had nothing to do with me getting smarter. The only new thing each time was the syntax. A for-loop is a for-loop and a contract is a contract, and the principles came along to every new stack while the syntax got relearned from scratch.
An LLM is just another author, one whose output looks plausible on the first scroll, and what matters is whether the principles hold underneath. The four moves below are what I run in my head in the seconds before reading a diff line by line.
Read for contracts
Scan the diff for the structures that lock the rules down:
- Interfaces.
- Enums paired with a sibling config map: one record per enum value, so a single lookup replaces an if-else chain.
- Schemas declared once and used everywhere they are needed.
If a contract exists, the system has one source of truth. If it does not, every place that uses the logic has to know the same rules on its own, and the first one to drift breaks the rest.
When I mark a diff too functional, this is the pattern: the code solves the task right in front of it with inline conditionals, repeated branches, and copy-pasted regex, when one shared abstraction would have replaced all of it. LLMs default to this because the model is answering the task in the prompt and will never pay the maintenance bill, and repetition costs nothing to an author who will not be around for the third feature.
DRY and KISS both depend on this. You can only avoid repeating yourself when a contract holds the rule in one place, and the same contract keeps things simple as the system grows.
This move over-fires. Not every piece of code needs a contract, and a helper the codebase will only call once, that nobody will need to extend later, is allowed to stay a helper. The question I ask is whether anyone else depends on this logic staying the same. If they do, it earns a contract, and if they do not, it stays inline.
Read for data flow through layers
Now look at how the diff flows through your system's layers:
- Frontend: user interaction, then components, then network.
- Backend: controller or blueprint, then service layer (business logic), then data abstraction such as an ORM or query layer, then the connector to the database or cache.
Each layer has a job, and each transition between layers is where one layer's contract meets the next.
The smell here is layer violation: business logic that has leaked into a controller, a SQL query inside a service, a fetch call inside a UI component that should be handed its data rather than asking for it. The code runs and the endpoints respond, and then the first time someone needs to change the data source or test the business logic in isolation, the bill for skipping the layers arrives. When I reject a diff as one that does not scale, this is nearly always the diff I mean.
Contracts and data flow are related but not the same. Contracts are the joints between layers, and data flow is the direction the work moves across them, so Move 1 asks whether the joint exists and Move 2 asks whether it gets respected.
This move over-fires too. A small script that fetches a config and prints a number does not need three layers, and the move only applies to systems that already have, or will soon need, layers. Apply it to a forty-line helper and you get grand architecture on a problem that did not ask for it.
Read for folder structure
Look at where the change lives in the tree. Does this file sit in a sensible module or feature? Does the diff scatter edits across folders that have no business changing together? Did the LLM create a new file next to the call site instead of putting it where the codebase usually puts that kind of code? Did common code get duplicated into a feature folder when it should have moved into a shared module?
Folder structure is where Moves 1 and 2 show up visibly. Most placement errors are contract or layering errors showing up in the file tree, and when generated code drops a new utility next to its first caller, that is the model taking the shortest path to a working answer. The placement is only the easiest tell of the principle being violated underneath.
This move applies hardest in 0-to-1 projects that are growing. I have run enough of them to know that structure starts to matter earlier than most teams expect, and the decisions worth making early are the ones that force you to think about modules, features, what is common, and what is extensible.
This move over-fires on a project that is too young to know its shape. If the codebase is two weeks old, the right move is keeping the surface area small until the structure becomes obvious, and rearranging folders before then is procrastination dressed as taste.
Read for what shouldn't exist
The first three moves check whether the right thing is there, and this one checks for things that should not be. Generated code is biased toward more code: more files, more wrappers. What I flag in every read:
- A try-catch around a synchronous helper that cannot throw.
- A defensive null check on a value the type system guarantees.
- A new abstraction over a one-line operation the codebase already calls inline.
- An interface for a class with one implementation that will never have another.
I find more code than the problem needs in nearly every LLM diff I read, so on this pass I ask the negative question: this helper has one caller and should be inlined, this enum has two values that will not extend, this defensive check guards a case that cannot happen.
The opposite mistake is pruning an unfamiliar but correct abstraction because you mistook it for bloat. If the codebase has chosen a pattern you do not personally like, that is not the same as the diff being wrong, and Move 4 prunes what the codebase never needed rather than what it has already decided.
When the playbook doesn't apply
Each move only fires when the codebase is in the right shape for it:
- Contracts apply when more than one place will use the logic.
- Layering applies when the system already has, or will need, a layered architecture.
- Folder hygiene applies when the project has lived long enough to know its modules.
- Prune applies when the diff has added more than the problem required.
The over-fire cases matter as much as the others:
- Apply Move 1 to a single-use helper and you get a pointless interface.
- Apply Move 2 to a one-file utility and you get three layers of nothing.
- Apply Move 3 to a two-week-old project and you waste a week.
- Apply Move 4 to a codebase whose conventions you have not earned and you knife working architecture you do not yet understand.
The moves are a scan, run in the seconds before the line-by-line read, and they tell you whether the diff is worth that read at all and what to look for once you start it.
Closing
I stay skeptical by default because what an LLM produces on its own has a ceiling, and the ceiling shows up fast: anything past a small MVP runs into scale and correctness problems the model could not have known to expect.
The market's answer is more automation: LLM reviewers on top of LLM authors, layered bots, policy as code, a category growing from two billion dollars to five billion by 2028. Tools can support the read, but they cannot do it for you, because the read means building the theory, and the theory only ever lives in a head.
What the market does not sell is internalized principles. One engineer fields many PRs a week now, the review queue is the bottleneck, and that is why I hold four moves in my head instead of a forty-item list: the four run in the seconds a queue allows, and the forty do not.
After the read, you have either built the theory the diff implies or you have refused to, and the refusal goes back to the author, human or otherwise.