·10 min read

The bottleneck moved: what actually changed when AI started writing my code

After a year of building production systems with AI in the loop, the hard part stopped being the code. It became the spec, the context, and the judgment to tell good output from plausible output.

AI-native developmentcontext engineeringsoftware architectureClaude Code

The most common thing I hear about AI coding tools is that they make you faster. That is true, but not in the way people mean it. They do not make you a faster typist with a robot autocomplete. They move the bottleneck somewhere else, and if you do not notice where it moved, you end up slower than before, with a codebase you do not fully understand.

I have spent the last year shipping real systems this way: a shift-rostering service migrated from Kotlin to Rust for an enterprise workforce platform, a from-scratch Rust reimplementation of a Chrome DevTools MCP server, an AI agent platform, a low-latency arbitrage engine in Go. In every one of them the code got cheaper to produce and the thinking got more expensive. That trade is the whole story, so let me walk through what actually changed.

The old bottleneck was typing. The new one is specifying.

For most of my career the limiting factor was implementation throughput. You knew roughly what to build, and the job was to turn that into correct, working code fast enough. Experience mostly bought you speed: fewer bugs, less time lost to dead ends, faster recovery when something broke.

That constraint is largely gone for the mechanical parts. I can describe a repository layer, an endpoint, a migration, a test suite, and get a working first draft in minutes. What I cannot do is get a good result from a vague request. The model will happily turn a vague request into confident, plausible, wrong code, and it will do it fast enough that you can accumulate a week of wrong direction in an afternoon.

So the work shifted upstream. The expensive artifact is no longer the code. It is the specification: the precise statement of what the system must do, what it must never do, which invariants hold, what the edge cases are, and how you will know it works. When I migrated the rostering service to Rust, the hard part was never the Axum handlers or the Redis calls. It was pinning down backward compatibility down to the byte, so that the new service behaved identically to the old one under every input the old one accepted. Once that was written down clearly, the implementation was almost mechanical. Before it was written down, no amount of model horsepower could have saved me.

This is why I describe my work as spec-first rather than AI-assisted. The AI is not the interesting part. The discipline of writing an unambiguous spec before any code exists is the interesting part, and it is the same discipline that separated good engineers from fast typists long before any of this.

More context is not more help

The first instinct, once you realize context matters, is to give the model everything. Paste the whole file. Load every rule. Attach the entire architecture document. Surely more information means better output.

It does not, and this is one of the least intuitive things about working with these tools. Adding context past a certain point actively degrades the result, for reasons that are baked into how the models work, not into how well you prompt them.

Two findings shaped how I think about this. The first is Lost in the Middle from a Stanford group (Liu et al., 2023), which showed that language models recall information best when it sits at the very beginning or the very end of the context and measurably worse when it sits in the middle. Performance follows a U-shaped curve. Bury the one constraint that matters in the middle of a 40-file paste and the model will treat it as background noise. The second is IFScale from 2025, which measured how well models follow instructions as you pile them up. Even frontier models drop toward 68 percent adherence once you hand them hundreds of simultaneous instructions, and they systematically favor the instructions you gave first. Your twentieth rule is being quietly ignored.

Put those together and the conclusion is uncomfortable: a bloated context does not just waste tokens, it hides your most important requirement inside a pile of things the model is now less able to follow. I have watched a model ignore an explicit constraint not because it was unclear, but because it was constraint number thirty in a wall of text, sitting in the dead zone in the middle.

a constraint buried here reads as background noise Start Middle End Recall accuracy Position in the context window
The "lost in the middle" effect: put the one thing that matters where recall is weakest and the model treats it as noise.

So my rule is the opposite of the instinct. Give the model the smallest set of high-signal context that lets it do the current step, and nothing more. Not the whole codebase, the two files that matter. Not the entire style guide, the one convention this change touches. When an instruction file grows past a couple hundred lines, I treat that as a design smell and split it: a short entry point that says what exists and when to reach for it, with the detail moved into references that get pulled in only when a task actually needs them. Curation beats accumulation every single time.

The judgment to reject plausible code is the moat

Here is the part that does not get said enough. When the cost of producing code drops to near zero, the value of being able to tell good code from bad code goes up, not down. The scarce skill is no longer writing the code. It is reading it with enough judgment to reject the ninety percent that looks right and is subtly wrong.

AI-generated code has a specific failure mode: it is locally plausible and globally naive. It will write a handler that works in the happy path and quietly mishandles the concurrent case. It will reach for the storage layer directly from the business logic because that is the most common pattern in its training data, not because it is the right boundary for your system. It will pick strong consistency where you needed availability, or the reverse, because it has no stake in the trade-off and no knowledge of your actual load. Reviews of AI-written code keep surfacing security and correctness issues at rates you cannot ignore, and none of those issues announce themselves. They look fine.

This is exactly where ten years of architecture judgment stops being nostalgia and starts being the job. Separating the core domain logic from the infrastructure shell so the business rules do not depend on the database. Knowing that an airline seat booking needs strong consistency and a profile avatar does not. Recognizing that a solution which scales vertically today will hit a wall you can see coming and designing for horizontal scale before you need it. The model can produce a candidate for any of these. It cannot tell you which candidate is right for your context, because judging trade-offs against a real system is precisely the thing it does not do.

The shift, put simply, is from writing every line to owning every decision. I generate a lot more code than I used to and I trust it a lot less on sight. Every non-trivial piece gets read against the spec, against the invariants, against the failure modes I already know are lurking. The AI drafts. The architecture, the boundaries, and the go/no-go on each decision stay mine, because those are the parts that determine whether the system survives contact with production.

The economics are a context problem too

One more thing that surprised me, because it connects the craft directly to the bill. The cost of working this way is dominated by how you manage context, not by how much you generate.

Modern coding assistants lean heavily on prompt caching: the stable prefix of your session, the tool definitions, the project instructions, get cached and reused across turns. Anthropic prices a cache read at a tenth of a normal input token, while writing that cache the first time costs a small premium. The practical consequence is that a stable, well-ordered context is not just better for output quality, it is roughly ten times cheaper to run. Every habit that keeps the prefix stable pays twice: the model stays focused, and the tokens stay cheap.

Base input 1.0× Cache write (first time) 1.25× Cache read (every reuse) 0.1×
You pay a small premium to write the cache once, then read it back at a tenth of the price on every turn that keeps the prefix stable.

The habits that break caching are the same habits that produce bad output. A five-hundred-line project instruction file that you keep editing invalidates the cache on every change and buries your real constraints. Wiring up ten tool integrations injects thousands of tokens of schema before the first real instruction. Running five unrelated tasks in one long session bloats the context with things that no longer matter. Cost hygiene and output quality turn out to be the same discipline viewed from two angles, which is a nice property: the thing that makes the work good also makes it cheap.

What compounds now

If I had to compress the last year into one sentence, it would be this: the tools got very good at answering, so the entire game became asking well.

Asking well is not a prompt trick. It is writing a specification precise enough that a fast, literal, tireless collaborator cannot misread it. It is engineering the context so the one thing that matters is not lost in the middle. It is holding onto enough architectural judgment to reject the plausible-but-wrong before it reaches production. None of those are new skills. They are the same skills that mattered when I was writing every line by hand, except now they are the whole job instead of the part you did between typing.

That is the good news for anyone who spent years learning to think about systems rather than just produce code. The leverage moved to exactly where that experience lives. The code was never the hard part. It just used to be the slow part, and now that it is fast, there is nowhere left to hide from the thinking.