The old argument for skipping the spec
For twenty years the case against heavy specification was good. Requirements change, documents rot, and — the strongest point — writing the code *was* the thinking. You discovered the ambiguities by hitting them. Halfway through the function you realised nobody had said what happens on a tie, so you went and asked. The typing was slow enough to be a thinking process.
That mechanism is gone, or at least much weaker. When a draft takes ninety seconds, the ambiguity does not stop you. It gets resolved silently, plausibly, in whatever direction the priors point, and you receive a confident answer to a question you never asked.
So the thinking has to move somewhere. It moves to before.
What a useful specification looks like now
Not forty pages. Usually one, sometimes three paragraphs. It has to contain the things a competent stranger could not guess.
Take a request that sounds complete: *add rate limiting to the signup endpoint.*
Every one of these is unspecified, and every one changes the code:
- Per IP address, per account, per device, or some combination?
- What window, and what limit? Ten per hour or three per minute are different systems.
- What happens at the limit? A 429 with
Retry-After, a silent delay, a CAPTCHA, a shadow ban? - What about shared addresses — a university NAT in Lagos, a corporate gateway in Jakarta, a mobile carrier in São Paulo putting a hundred thousand users behind one IPv4 address? Per-IP limiting will lock out a whole campus.
- When the limiter's own storage is unreachable, does signup fail open or fail closed?
That last question has no technically correct answer. Fail open and an outage of the limiter becomes an open door for abuse. Fail closed and an outage of the limiter becomes an outage of signup. It is a business decision with a security consequence, it depends on what your company can survive, and nothing that has not sat in your incident reviews can make it for you.
A spec that answers those five questions turns a vague request into work that can be done correctly by anyone, or anything, and checked afterwards.
Write the decision, not just the requirement
The sentence that survives longest is the one that says why:
## Rate limit: POST /signup
Key: (ip, account_email_domain). 5 attempts / 10 min, sliding window.
Over limit: 429 + Retry-After. No CAPTCHA in v1.
On Redis unavailable: FAIL OPEN.
Why: signup outage costs more than a burst of abuse we can clean up
afterwards. Revisit if abuse volume exceeds ~200 fake accounts/day.
Out of scope: password reset, OAuth callback, mobile app endpoints.Six months from now, someone will see the fail-open branch and assume it is a bug. Those two lines are the difference between them fixing a decision and them respecting it. This is what architecture decision records were always for; the practice just went from nice-to-have to load-bearing.
The out-of-scope line is doing real work
Generated changes sprawl. Ask for a rate limiter and you may get one on four endpoints, plus a config module, plus a metrics wrapper. Each addition is defensible in isolation, and together they are a change nobody scoped and nobody can review properly.
Stating what is out of scope is the cheapest control you have over blast radius, and it works on human collaborators too.
One artifact, three jobs
The interesting part: the spec is now also the prompt, and the acceptance criteria are now also the test names. The same page tells the model what to build, tells the reviewer what to check against, and tells the test suite what to assert. When they are one artifact, they cannot drift apart.
Which brings the one rule that makes this work at all: the spec lives in the repository, next to the code, and changes in the same commit. A specification that drifts is worse than none, because it is wrong with authority. If updating it is a separate ceremony in a separate tool, it will drift by the second week.
The honest caveat
None of this means writing a design document for a two-line fix. The rule of thumb: write the spec when a wrong guess would be expensive or invisible. Renaming a variable, no. Anything touching money, permissions, deletion, or another company's API, yes. Anything where the phrase "it depends what we mean by" comes up in conversation, yes, and write down which meaning you picked.
Before you move on