Do I Still Need to Read Code? Rethinking Abstraction in the Age of AI
AI agents built an admin interface for a customer project, and review agents caught flaws that Senior Software Engineer Daniel Willig missed. But the question is: does a developer still need to read code? Daniel explains which checks can safely move to agents and automated rules, and which code needs an accountable human reader.
Key takeaways
- AI agents can now write most of the code in a project. The real question is who checks it, and how.
- On a recent customer project, Daniel split the checking three ways: automated rules checked the structure, review agents checked the detail, and he checked what the customer actually needed and who could access what.
- The result is software built at agent speed, with the parts that protect data and access still reviewed by someone accountable for them.
I recently built an admin interface for a customer project without writing a single line of code myself. One set of AI agents wrote the implementation and a second set, the review agents, examined each change, while a static code analyzer and a linter enforced the rules I had set. I reviewed the changes as carefully as I could, and the review agents still found more than I did, in a fraction of the time.
They regularly pointed out edge cases I had not considered at all, and once a flaw in a decision I had made myself. Early in the project I had specified, without thinking it through, that every failed login should create an audit entry, a record in the log that tracks which user did what and when. A failed login has no authenticated user, so these entries had no actor and recorded almost nothing of use. Worse, anyone could fill the audit database simply by sending login attempts with wrong credentials. A review agent flagged this, while I had never questioned my own requirement.
Experiences like this made me wonder whether reading code is still part of my job, or whether my job has become checking that what comes out matches what went in. I tend to agree with the view that AI is another level of abstraction. Programmers stopped reading assembly once compilers were trustworthy, and they kept caring about correctness by moving their attention up a level to the source code. If agents reliably turn intent into working code, attention could move up again, to the specification and the observed behavior.
I think the analogy holds for most of the code but breaks in a few places, and where it breaks tells us something about why we build abstractions in the first place. This question matters beyond developers. When agents write the code, the people paying for it need to know what still gets checked, by whom, and what guarantees they get in return.
Where the abstraction analogy holds and where it breaks
A compiler can be trusted without reading its output because its translation is deterministic and because its input, the source code, defines the behavior completely. We tend to credit the tool, but much of that trust comes from the precision of the input.
A ticket or a prompt is far less precise. Natural language leaves gaps, and an agent fills them with choices that look reasonable, which I only see once they appear in the code. An agent builds what it is asked for, gaps included, so the quality of the requirements now does much to decide the quality of the software. Comparing input and output can replace reading code only if the input is exact enough to compare against. In my project that precision came from detailed acceptance criteria in the tickets, from which the agents derived the tests. The criteria now play the part source code used to play, so they deserve the careful reading the implementation used to get. The derived tests deserve a second look as well, because a test written from a criterion checks the agent’s interpretation of it, and any gap in the criterion carries over into the test.
The audit problem is worth a closer look, because the flaw was in my requirement and the implementation followed it faithfully. The agent found it by reasoning about consequences: who can trigger a failed login, and what the resulting entry contains. Review agents can therefore question intent, provided that intent has left a trace in the code. What they cannot see is intent that never reaches the repository, such as an expectation the customer holds but nobody wrote down. If the reviewing agents and the agents that wrote the code are built on similar models, they may also overlook the same things, so their agreement counts for less than two independent people agreeing.
Authorization is the other weak spot, and an admin interface consists largely of authorization. A test can confirm that an administrator may edit a record. Showing from the outside that no user with a lesser role can do the same is much harder, because that property concerns every possible path through the code and no set of tests exercises all of them.
Why we abstract: Three constraints
Fred Brooks distinguished essential complexity, which belongs to the problem, from accidental complexity, which comes from our tools and methods, in his 1986 essay "No Silver Bullet." Moseley and Marks built on that distinction in their 2006 paper "Out of the Tar Pit." Their distinction helps decide whether an abstraction removes complexity or only moves it elsewhere. It does not say whom or what the abstraction serves, which is the question agents raise. For that I find it more useful to ask what an abstraction protects against, and I see three distinct constraints behind the abstractions we use.
The first is human working memory. We can only keep a handful of interacting ideas in mind, so we hide detail behind names and familiar patterns and cut logic into small functions. John Sweller’s cognitive load theory, Green and Petre’s cognitive dimensions framework, which rates notations by properties such as how hard they are to change and how many dependencies they hide, and Felienne Hermans’ book The Programmer’s Brain explain why this helps. The price is indirection, since finding out what happens means following calls through interfaces and configuration.
The second is the model’s own context. Models read much more code at once than we do, and some of this compression is unnecessary for them. Their context is still limited, and every token, the unit in which models read text and are billed, has a cost. In a large codebase a model also has to find the right files before it can do anything useful with them, and clear names and coherent modules help with that search. Many abstractions built for people therefore help models too, only at different thresholds.
The third constraint has nothing to do with who reads the code. David Parnas argued in 1972 that modules should hide design decisions likely to change, so that each change stays local. Melvin Conway had observed a few years earlier that systems take on the communication structure of the organizations that build them, which is why many boundaries in software exist so that separate teams can work without constant coordination. Security boundaries, which limit what each component may access, belong in this group as well, and so do the points where tests can replace real dependencies such as a database. These abstractions protect properties of the running system and of the way it is built, and an agent writing the code changes nothing about why they are needed.
Most abstractions serve more than one of these constraints at once. What matters is which one justifies a given abstraction, because one that only supports human reading is worth questioning when agents do most of the reading and writing.

Formatting serves only human readers, while names and small functions also help models find their way through a codebase. The abstractions that reach into the right column stay necessary whoever writes the code.
What the early evidence says
Research that tests these constraints directly is still sparse. In a 2023 study, Jain and colleagues at UC Berkeley rewrote training data for code generators to be more structured and readable, and found that the resulting models generated better code. Models seem to benefit from the same structure humans do, which is not surprising given that they learned from code written for humans.
Models also do not reliably find the right amount of abstraction on their own. The authors of KISS Sorcar, a recent coding assistant, describe models adding helper classes and extra layers of indirection that make the code harder to debug, and they counter it by instructing the model to prefer simple code. A model that copies patterns from its training data without judging when they are warranted can produce abstraction that none of the three constraints justifies.
Formatting is where human and model needs clearly diverge. One study measured how much of a model’s token budget goes to whitespace and layout, which exist almost only for human readers, and proposed tooling that shows people formatted code while giving the model a compact version. It is the cleanest example I know of an abstraction that serves only human cognition and can be separated from the code itself.
Developers themselves have not given up on readability. In a 2025 survey by Atlassian, most of its developers said readable code remains essential for maintenance and collaboration, even as AI writes more of it. As long as people own and maintain the code, that is a reasonable position.
A test case: Hexagonal architecture
When I started the project, I decided it would follow a hexagonal architecture. I kept doubting that decision during implementation, mainly because I could not rule out that I had chosen it simply because I always do.
In a hexagonal architecture, also known as ports and adapters, the domain logic defines interfaces for what it needs, called ports, and adapters implement them for concrete technology such as a database or a web framework. The usual arguments for it are that infrastructure can be swapped without touching business logic and that the domain can be tested without a running system. Both presuppose a substantial amount of domain logic and a real chance that adapters will change. An admin interface mostly creates, reads, updates, and deletes records behind access checks, so neither argument was strong in my case.
What I had not expected was how useful the architecture’s rules would become once agents were writing the code. Hexagonal architecture comes down to a few dependency rules, chiefly that the domain never imports from adapters and that adapters depend only on ports. Tools such as ArchUnit, dependency-cruiser, or eslint-plugin-boundaries can check such rules mechanically, and in my project custom linter rules caught the typical boundary violations, such as domain logic importing an adapter directly instead of going through its port. A developer who works on a project for months internalizes its conventions and applies them without being told. An agent starts each session without memory of the decisions made in earlier ones, so its code drifts from the intended structure even when every individual change looks reasonable. The conventions therefore have to live outside the agent, in rules checked on every change, which makes an architecture with checkable rules more valuable in an agent-driven project than in a traditional one.

The solid arrows show the only permitted direction of dependency. The dashed one is the kind of violation the linter rules caught in agent-written code.
The same approach worked for authorization, which I described earlier as the hardest property to verify from the outside. Every route in the admin interface is wrapped in a permission wrapper, a function that declares which permission a user needs before the route can be used. One linter rule required every route to have such a wrapper. A public route was no exception and needed an explicit PublicPermissions wrapper to pass the check. Many systems are open by default, so that a route without a check is simply accessible and a forgotten check goes unnoticed. With this rule, every route was closed until someone decided otherwise, and permissions were always declared, never inferred from the absence of a check. The rule cannot tell whether the declared permission is the right one. It does guarantee that no route escapes the question, and it reduces my part of the review to going through a list of routes and the permissions assigned to them, instead of searching the code for paths that might have been left unprotected. For the customer, this means that no page or action in the system can pass the checks without someone having decided who may use it. The full list of who can do what is also something they or their auditors can review for themselves, instead of taking it on trust.
In simplified form, with names chosen for illustration, the rule distinguishes between these three routes:
// Admin route: only users with the right role may list accounts
export const GET = withPermissions(AdminPermissions.ReadUsers, async (request) => {
return listUsers(request);
});
// Public route: still needs an explicit wrapper to pass the linter
export const POST = withPermissions(PublicPermissions, async (request) => {
return login(request);
});
// Rejected by the linter: no permission wrapper at all
export const DELETE = async (request) => {
return deleteUser(request);
};
The usual objection to hexagonal architecture is the amount of code it takes, with ports, adapters, and mappers for every feature. An agent writes that code at almost no cost. The indirection is still there when something goes wrong, though, and reviewing a change that crosses several layers still takes longer.
I cannot conclude from this that hexagonal architecture in particular was the right choice. The rules caught mistakes, which tells me that enforced boundaries paid off, but a simpler layered structure with the same kind of import rules might have caught them as well. Either way, the architecture earned its place by protecting the structure of the system, which belongs to the third constraint and was not what I had in mind when I chose it.
A division of labor
My current answer to the opening question is to split the checking according to what each participant does well. The static analysis and linting rules check structure, and they check it the same way on every change. The review agents check detail, including edge cases and security problems like the audit entries, faster and more thoroughly than I can. What is left for me is the intent that never reaches the code: whether the specification describes what the customer needs, and whether the properties the system depends on actually hold.

Each check covers something the previous one cannot see. Only the last step still involves reading code, and only the parts that enforce guarantees.
So I no longer need to read all of the code, but I still need to read the parts that enforce guarantees, such as authorization checks, access to data, and interfaces other systems rely on. A mistake there is often invisible from the outside, and neither a linter nor a review agent knows what was intended. For the rest I can judge behavior, as long as that behavior is pinned down by acceptance criteria detailed enough to derive tests from, and I have read both the criteria and the tests with care.
The customer holds me responsible for the result, and an agent’s approval does not change that. It is one more check in the process, and a good one, while the judgment about what should be built remains mine.
Trust has to be built around the agent
I still think AI is another level of abstraction, though it differs from a compiler in an important way. A compiler is trusted because its input is precise and its translation deterministic. An agent starts from imprecise intent, so trust has to be built around it with precise acceptance criteria, the tests derived from them, and mechanically checked rules, along with human attention on the code that enforces the system’s guarantees. With those in place, checking output against input is a reasonable replacement for reading every line.
The project also showed me that an abstraction can prove useful for a different reason than the one it was chosen for. I picked hexagonal architecture partly out of habit, and it turned out to matter because it gave the agents boundaries that could be checked automatically. I suspect many abstractions we consider indispensable deserve a similar reassessment, and that some of them will turn out to exist only because they helped us read.
How long people will remain the main readers of production code is something I cannot answer. For now we still review changes and investigate incidents, so abstractions that make code readable keep much of their value. If that role shifts further toward specifying and verifying, readability might become something produced on request, for example, an agent explaining or restructuring a particular part of the code for the person who needs to understand it, much as formatting can already be applied independently of the code. Whether that works well enough to rely on is still an open question.