← Back to Blog
6 min read
Share

The Spec Is the Work Now

The Spec Is the Work Now

I used to think specs were bureaucracy. Someone would write a six-page document describing a feature, everyone would skim it, and then the engineer would build something adjacent to it and the document would rot in a folder nobody opened again. The code was the artifact. The spec was overhead you tolerated at companies large enough to need it.

That calculation has quietly inverted, and I don't think enough people have noticed.

The bottleneck moved

The old ratio was roughly: an hour thinking, a day typing. Writing the code was the expensive part, so anything that didn't directly reduce typing time felt like a detour. A vague spec was survivable because you'd figure out the details as you wrote, and figuring-out-while-writing was most of the job anyway.

The new ratio is: an hour thinking, ninety seconds generating. When the implementation is nearly free, every ambiguity you leave in the description gets resolved by something that isn't you — confidently, plausibly, and often wrong in a way that takes an hour to find.

The thinking didn't get cheaper. It just became the only expensive thing left.

What a bad prompt actually costs

Here's a request I made a while back, more or less verbatim:

Add a rate limiter to the API routes.

Ninety seconds later I had a rate limiter. It was a fixed-window counter in a module-level Map, keyed by IP, 100 requests per minute, applied to every route.

Every single one of those decisions was wrong for my situation, and none of them were wrong in an obvious way:

  • Module-level Map means the limit resets on every cold start and each serverless instance keeps its own count. On a platform running eight instances, my "100/min" was really 800/min.
  • Fixed window lets someone send 100 requests at 11:59:59 and 100 more at 12:00:00. I wanted a sliding window.
  • Keyed by IP is wrong for authenticated routes, where I wanted per-user, and wrong behind a proxy, where req.ip is the proxy's.
  • Every route included the expensive LLM endpoint that should have had a much tighter limit, and the health check that should have had none.

The code was clean, typed, commented, and passed review at a glance. It just didn't do what I needed. And crucially: all four problems were in my request, not in the output. I asked for "a rate limiter" and got the median rate limiter.

Compare the version I should have written:

Add rate limiting. Storage: Upstash Redis (already in the project), so limits hold across serverless instances. Algorithm: sliding window. Key: user ID for authenticated requests, IP from x-forwarded-for for anonymous. Limits per route group: /api/llm/* 10/min, everything else 60/min, /api/health exempt. On limit: 429 with a Retry-After header. Fail open if Redis is unreachable — log it, don't block the request.

Same ninety seconds of generation. The difference is that the second one is correct, and it took me about four minutes to write.

That's the actual trade. Four minutes of specification against an hour of debugging something that looked finished.

Specs got shorter, not longer

The important thing is that this isn't a return to the six-page document. The old spec was long because it was a communication protocol between humans across weeks — it needed context, background, rationale, and enough redundancy to survive being half-read.

The new spec is a hundred and fifty words and it's aimed at something that will read all of it, right now. No preamble, no background section, no rationale. Just constraints.

What earns its place:

  • The decisions you actually care about. Storage layer, algorithm, key strategy, limits. If you don't state them you're voting for the median choice.
  • What's already in the project. "Upstash Redis (already in the project)" prevents a new dependency for something you already pay for. This one line saves more grief than any other.
  • The failure behaviour. Fail open or fail closed? This is never inferable, it's always a judgment call, and it's the one that bites you in production at 3am.
  • The exceptions. Health checks, admin routes, the internal cron. Exceptions are where generated code is weakest, because they're the part that isn't in the training data for "rate limiter".

What still doesn't earn its place: anything about how to write the code. Naming conventions, file structure, whether to use a class. Those either don't matter or are already visible in the codebase.

The uncomfortable part

Writing a good spec requires knowing what you want, and a lot of the time you don't.

This used to be hidden. You'd start typing, and the act of writing the code would surface the questions one at a time, in an order that made sense, at the moment each one became relevant. You'd discover you needed a sliding window when you got to the part where you were writing the window logic. The implementation was a thinking aid.

Generate the whole thing in ninety seconds and you skip the entire discovery process. You never hit the moment where you'd have realised the window strategy mattered. You get an answer to a question you didn't know you were asking, and no signal that a decision was made on your behalf.

The only way out is to do the discovery deliberately, up front, before you have code to look at. Which is harder, because there's nothing concrete to react to.

The habit I've landed on is a fast pass down four questions before any non-trivial request:

  1. Where does the state live, and what happens when there's more than one instance of this thing running?
  2. What's the failure mode, and is failing open or failing closed correct here?
  3. What's already in the project that this should reuse instead of introducing?
  4. What's the exception — the one case that shouldn't follow the general rule?

That's two minutes. It catches most of what the median implementation gets wrong, because those four are precisely the dimensions where "reasonable default" and "right for this codebase" diverge.

Where this leaves the code

None of this makes the code unimportant — you still have to read it, and reading it is still where you catch the things your spec didn't anticipate. But the code has stopped being where the leverage is. It's the output. It's downstream.

The leverage is in the paragraph you write before you hit enter. That paragraph used to be a formality that preceded the real work. It is now, fairly literally, the work.

The good news is that it's a skill that transfers in both directions. The engineers I know who write the sharpest prompts are the ones who were already good at writing tickets — the ones whose issues you could pick up cold and just implement. That was always a rare skill and a slightly underappreciated one. It's about to be the main event.

Related: Prompt engineering secrets · The three-prompt rule · When to stop prompting

Get the good stuff

New tools and posts, occasionally. No spam.