Burning Time Instead of Tokens
Thinking about what I’ve been experiencing day to day, I work on a production product whose codebase is huge and badly architected. We don’t have a quality technical specification covering what already exists, let alone the new features. We don’t have a code style, that thing that normally organises how humans will read each other’s code. We don’t have an integration test suite covering every case, including the ones that fail and produce functional bugs in production. That’s the ground I’m starting from, because I think here concrete experience matters more than theory.
Using AI to implement a complete spec led me to understand something I didn’t expect: keeping a human in the loop makes the work purely slow, mainly because of review. And the reason becomes clear when you look closely. To approve what the model generated, the human needs to understand the code, understand the style, and feel comfortable with the architectural and implementation decisions the model made. So they end up spending time criticising the model’s work according to their own criteria, and those criteria are almost always subjective and were never specified, because you can see there’s a lack of specification even about how the architecture should work. In other words, we get stuck measuring the model’s output against a ruler that lives only in one person’s head. It’s not generation that’s expensive, it’s that evaluation against a phantom standard.
The recommendation
The recommendation, then, would be to approach changes with well-specified tests, stop doing code review with the lens that a human will maintain the code afterwards, and produce quality documentation about the current state of the architecture, the functional and non-functional requirements, as well as the code style. I think investing time exclusively in this aspect can greatly accelerate adoption and development with the model.
But I need to be honest with myself, otherwise this becomes empty enthusiasm. The sentence “don’t review as if a human will maintain the code” is the riskiest part of what I’m saying, and I don’t want it read as “nobody needs to understand anything anymore.” The sober version is different: review doesn’t disappear, it moves up a layer. Instead of reviewing every generated line, you start reviewing the spec and the tests, which are the real source of truth, and the code becomes a build artifact. It’s the same move we already made once, when we stopped reading assembly and started trusting the compiler. Trust migrated from the result to the tool and to the higher-level source. What I’m proposing is to repeat that leap one more layer up. Someone will still debug production at 3 a.m., someone still answers for the system, it’s just that human effort stops being diluted line by line and concentrates where the return is largest.
The objection the team will raise
I know what the team’s most visceral resistance will be, because it’s legitimate as a feeling: you can’t review all the code the model generates. And my answer to that is a question. Do you already do that today with your dependencies?
I like to use the Rust example. A project using Tokio carries tens of thousands of lines of an async runtime running at the heart of the system, lines nobody on the team has read and probably never will, and even so we trust it, ship to production and sleep at night. Go up a level, to infrastructure. Did you review Kafka’s code before publishing the first event to it? Did you read RabbitMQ’s source, Postgres’s, TimescaleDB’s before trusting your data to them? Of course not. You trust them because they work based on the functional and non-functional specifications you have for each one, that is, documented behaviour, guarantees, a contract, plus a long track record and your own tests at the boundary.
So the criterion was never “I read every line,” it was always “I have a contract I trust and a way to verify behaviour at the edge.” Reading line by line was just a substitute we used for our own code, for lack of anything better at hand. The code the model generates is just more code in the same category, code you didn’t write. So the honest reaction isn’t to demand the impossible, it’s to give that code the same you give Postgres, a clear contract (the spec) and verification at the boundary (the tests). One caveat so I’m not lazy with the analogy: Tokio and Postgres took a beating from millions of users over years, and the module the model just generated has zero track record. So trust can’t come from reputation yet, it has to come entirely from the contract and the verification, which raises the bar on tests and spec rather than lowering it. Which is, conveniently, exactly what I’ve been arguing.
Language becomes a detail
There’s a consequence of all this that I find the most interesting part. If the specification is the source of truth and the code is a build artifact, then the programming language stops being a decision carved in stone and becomes a rendering target. With a complete and precise enough spec, you could have the code in any language. That’s what I call transpile with steroids. The idea isn’t to take Rust code and hope the model guesses the equivalent C, it’s to have a description of behaviour so precise that the code, in any language, is derived from it. In the same way, you can generate code from a formal language like F*.
But here I need to separate two things that are easy to confuse, because they give very different guarantees. The guarantee you receive depends on where your spec sits on a spectrum. On one end there’s prose the model interprets, which is fast, flexible, zero guarantee, and the output is plausible and needs tests to be trustworthy. On the other end there’s a formal language like F* or TLA*, or a type system rich enough, where rendering can come with proof attached. Most of us live in the middle, and knowing where you are on that spectrum is knowing how much you can trust the output without rereading every line. The dream of generating the system in any language is real, the fine print is that fidelity is a function of formality.
Feeding the model is the same as onboarding
I think the best way to see the model is as a new collaborator. Competent, fast, tireless, and with absolutely no implicit context about our domain. And then it becomes obvious: what you hand a new person on their first day is exactly what the model is missing, that is, documented architecture, functional and non-functional requirements written down, style guide, test suite serving as a contract. When you don’t have that, every interaction with the model repeats the cost of onboarding, over and over. That’s why I say that, without specification, we keep burning time instead of burning tokens.
And there’s a more serious version of the same disease, which is when a company writes systems as an extension of people. Even having the system, it still depends on specific individuals, with specific knowledge, for the thing to keep working. To me that’s a grotesque mistake, because the reason for having a system is precisely the opposite, it’s to automate and free the human to do everything else. A system that chains the person who understands it to its own operation did the opposite of what it promised, it’s a manual process disguised as software. And notice that the knowledge that keeps that system alive lives in heads, not in text, which is the same knowledge missing from the spec, that the new hire doesn’t have and that the model can’t read. It’s the same hole seen from three angles, and writing that knowledge down fixes the three at once.
Architecture is a choice, and today it became cheap
Another thing I think is worth rethinking now is architecture. It was always a choice, just an expensive choice to do well, and because it was expensive it ended up being postponed. Take a financial system, which is the example I like most. Modelling with event orientation isn’t a fad, it’s fidelity to the real world, because the world happens in events along a timeline, and money is literally a sequence of entries, not a number. If you store only the last state, like the current account’s final balance, you threw away the history. If you store the event stream, the current state becomes a projection you recompute whenever you want, and as a bonus you get a system capable of travelling through time and information, that is, reconstructing any past state, auditing, reprocessing, answering questions you hadn’t even asked when you wrote the code. It’s a rich system instead of an amnesic one.
And this kind of decision, choosing between event sourcing, CQRS, event orientation, actor model or a good old monolith, is exactly what’s worth doing deliberately, at the start, using the model. Especially because it has a huge framework of patterns to choose from and puts the alternatives and tradeoffs on the table in minutes. This attacks one of the most common deaths of software, which is the system that is born with good intentions and ends up as a big monolith that wanted to be a few microservices. This happens for several reasons, and one of them is confusing what was supposed to be an MVP or proof of concept with a finished product, and then the scaffolding that was there just to test an idea becomes structural and nobody goes back to replace it. When we use the model, the MVP and proof-of-concept phase becomes much more robust, we can render a throwaway but well-structured prototype, or even explore two real architectures before committing. The classic excuse, “it’s just an MVP, we’ll do it properly later,” loses force when doing it properly costs tokens instead of months. The only caveat, and I need to say this, is that more options also means more rope to hang yourself with, because the model will happily build a cathedral of event sourcing for a CRUD that didn’t need it. The framework is only worth it when it comes with judgment about what the domain actually asks for.
SOLID, Clean Code and hexagonal still hold, for a new reason
It’s also worth talking about SOLID, Clean Code and hexagonal architecture, because I think these things are good and sometimes we forget why. They exist because they made code communication within the team much more dynamic and assertive, brought robustness to the technology, and avoided technical debt and the natural wear of systems. At bottom, they were largely a discipline for human cognition, because we can only hold so much in our heads, so we decouple, name well and isolate responsibilities to be able to reason about a piece without carrying the whole system along.
The model follows these principles naturally and cheaply, because they’re among the best-represented patterns in what it learned, and because you can express them as constraints that it honours when you ask. And here’s the part I find non-obvious: the reason these principles are good changes, and they become more valuable, not less. Low coupling and clear boundaries used to be the way the human reasoned locally, and now they’re also the seams through which you divide the work for the model and verify each piece in isolation. Hexagonal architecture, with its ports and adapters, hands you exactly the delimited, testable units that automatic generation loves. In other words, the framing moves from readability, which was for the human to read, to verifiability, which is for you to generate and check in pieces. Same principles, updated reason.
I still need to be honest on two points
Before closing, two points I don’t want to sweep under the rug. The first is that there’s an uncomfortable circularity: writing a good spec for a large, messy system requires the same deep understanding that review used to give me, so the bottleneck doesn’t vanish, it shifts forward. The difference is that you pay once and reap always, instead of paying on every pull request, which is why to me it’s worth it, but it’s an investment, not a shortcut. The second is that tests only cover what you imagined testing, and human review used to catch what nobody specified, especially non-functional requirements like performance, security and behaviour under partial failure, which are much harder to capture in tests than functional ones. Putting them both in the same bucket would be lying to myself.
The Joel Test at 26
Finally, I think it’s worth revisiting the Joel Test, which is now an adult of 26 years. In 2000, Joel Spolsky published twelve binary questions to measure in three minutes whether a software team was serious, and it’s still almost entirely valid, except it was written for a world in which the reader of the code was always human. We can keep the same mechanic and measure something else, which is whether your organisation is ready to develop with the model:
- Is the current architecture written in text that a model can consume in full?
- Is there an executable test suite that covers the failure paths, not just the success ones?
- Are the functional and non-functional requirements written down somewhere?
- Is the code style enforced by tooling, like formatter, linter and types, instead of personal taste?
- Can an agent run build and tests with a single command, in isolation?
- Is there a way to hand the model the right slice of the code without drowning it in context?
- Can you evaluate the output objectively, tests pass, types close, properties proved, before looking with subjective criteria?
- Is the specification treated as the source of truth and the code as an artifact?
- Is there a record of architectural decisions, with the why and not just the what?
- Does human review happen on the spec and the tests, and not on every generated line?
- Does a new collaborator, human or model, reach the first useful commit from just what’s written?
- Do you measure success by tokens spent until green, and not by hours of review?
Just like with the original test, I bet most teams will score two or three, and to me that’s exactly the point. The nice difference is that in the Joel Test the questions were about discipline for humans to collaborate with each other, and here they are about making our domain legible to an intelligence that doesn’t share our implicit context.
What’s left
I think we’re in a moment of deep reflection, and it’s worth diving into the paradigm of how we approach the model and how we bring it into the domain. It should be fed with as much specification as possible to be aligned with the way we expect a system to behave, and this is the same approach as how we should onboard a new collaborator. It’s the same old problem, specification, except now with a direct, measurable economic consequence. Before, lack of spec was a silent debt that appeared months later in a bug. Now it appears right away, in every cycle with the model, as human time spent compensating for a document that was never written. The good news is that this effort isn’t a hack to please the machine, it’s the same thing that makes the system understandable to anyone who shows up later. The difference is that now I have a cheap, immediate reason to finally do what should have been done already. Without it, we keep burning time instead of tokens.
Reference
Joel Spolsky, “The Joel Test: 12 Steps to Better Code”, 2000. Available at https://www.joelonsoftware.com/2000/08/09/the-joel-test-12-steps-to-better-code/.