The thousand token rule is free for a limited time. Available also in epub and pdf formats.
← Details

The thousand token rule

This course is fully available for free. Sign in to access all chapters.

Requirements as filters

A requirement like "the system should handle user authentication" could describe a thousand different implementations. Password and email? OAuth? Magic links? Session duration? Rate limiting? Each decision point multiplies the possibilities.

Good requirements work like filters that narrow the solution space. "The system authenticates users via email and password, returning a JWT token valid for 24 hours" eliminates hundreds of possibilities. Add "failed authentication attempts are rate-limited to 5 per hour per IP address, passwords must be at least 12 characters, tokens include user ID and role claims" and you've constrained the space enough that different implementers would produce functionally equivalent systems.

That's the goal: constrain until there's only one reasonable way to implement the feature. Not to specify every line of code, but to eliminate ambiguity that causes expensive rework during implementation.

The five questions

Every requirement needs to answer: what does the system do, what inputs does it accept, what outputs does it produce, how does it handle errors, and what does it explicitly not do?

The behavior specification needs completeness. "Creates a new user account" seems clear until you ask whether it sends welcome emails, validates email addresses, or checks for duplicates. Each unspecified detail becomes a decision point during implementation, where changing your mind costs $0.078 per iteration instead of $0.014.

Inputs need precision. "An email address" leaves open whether you accept all valid RFC 5322 addresses or some subset, what the maximum length is, how you handle international characters. Edge cases come from inputs, and every vague input spec becomes an argument during code review or a production bug when someone enters data you didn't anticipate.

Outputs need equal precision. Does a search function return an array or a paginated response? Are results sorted? Include scores? Maximum count? These decisions affect both implementation and every piece of code that consumes the endpoint.

Error handling is where specifications typically get vague. "Handles errors appropriately" means nothing. A database failure is different from invalid input is different from authorization failure. Each needs a specified response, or you get implementation lottery where the AI might return 500 for everything, leak information in error messages, or silently fail.

The negative requirements prevent scope creep. Without explicit exclusions, implementers add features they assume will be needed. "User registration does not send welcome emails" and "search does not include archived items" make boundaries explicit and cost nothing to write.

Edge cases up front

Production traffic includes empty strings, null values, fields exceeding length limits, Unicode, SQL injection attempts, concurrent updates, constraint violations, and malformed data. Each of these is predictable and will happen.

Deciding how to handle them during implementation means understanding business requirements, making a decision, implementing it, testing it, and possibly revising. Deciding during requirements means writing down the policy in 15 minutes.

Walk through boundary conditions for each input: minimum, maximum, empty, null, duplicates, invalid formats, malicious content. For each boundary, specify the expected behavior. A display_name field might accept 1 to 50 characters stored as provided, treat empty string and null as clearing the field, reject over-length with "Display name too long," reject whitespace-only with "Display name cannot be whitespace," accept Unicode as UTF-8, and reject HTML tags with "Display name contains invalid characters."

One field, fully specified. Complete requirements do this for every field. It's tedious, but tedious at $0.014 per iteration beats tedious at $0.078 with a 6.5x defect multiplier.

Behavior, not implementation

"The system caches API responses using Redis" is an implementation detail, not a requirement. The requirement is "API responses are cached for 5 minutes to reduce database load." This specifies what, why, and the constraint while leaving technical planning free to evaluate Redis, memcached, in-memory caches, or database-level caching based on actual tradeoffs.

Requirements that specify implementation can lock you into wrong choices. "Store session data in localStorage" fails when sessions exceed browser storage limits. "Sessions persist across browser tabs" specifies the constraint and lets technical planning choose the mechanism.

A complete example

Here's what complete requirements look like for password reset via email. The feature lets users regain access when they forget passwords through an email-based flow. It excludes SMS reset, security questions, and admin-initiated reset.

Request reset: user provides an email address validated against RFC 5322. System generates a cryptographically random 32-byte token, stores it with 1-hour expiration, sends email with reset link. Response always returns success to prevent email enumeration. Rate-limited to 3 requests per email per hour, violations silently return success. Invalid email format returns 400.

Complete reset: user provides token as 32-byte hex string and new password 12-72 characters. System verifies token exists and hasn't expired, hashes password with bcrypt cost factor 12, updates user record, invalidates token. Success means user can log in. Invalid/expired token returns 400 "Invalid reset link." Password outside length constraint returns 400 with specific message. Database failure returns 500.

Tokens must use crypto.getRandomBytes, not Math.random. Reset emails delivered within 5 minutes under normal load.

That's 400 tokens. You can iterate on it 30 times for the cost of implementing it once. The iteration surfaces gaps: "What prevents reset request spam?" (Rate limiting.) "Should tokens invalidate after use?" (Yes, specified.) "What if password is only whitespace?" (Add to spec.)

Stress testing with AI

Write a draft requirement, then ask specific questions. "What inputs could cause this to fail?" gets a concrete list. "What happens if the database is down?" forces you to handle that scenario. "What prevents abuse?" surfaces rate limiting and validation needs.

The AI enumerates possibilities. Many are irrelevant, some are obscure edge cases you don't care about, but several are real gaps. Mark which ones matter, specify how to handle them, update requirements. Draft, stress-test, refine, repeat. Each iteration takes five to ten minutes. Three to five passes typically gets you complete coverage.

Specificity matters. "Are these requirements complete?" gets vague reassurance. "What happens when two users update the same record simultaneously?" gets concurrency issues you need to handle.

The completeness test

Could a developer who has never seen this feature implement it from these requirements alone, without additional context? If not, the requirements aren't complete.

"Handle user authentication" fails immediately. The developer makes dozens of decisions. "Authenticate via email/password with JWT tokens valid for 24 hours, rate-limited to 5 failed attempts per hour per IP, passwords minimum 12 characters" passes. A competent developer could build this without clarifying questions.

The test also catches premature implementation details. Requirements that specify "use Redis for caching" provide mechanism without constraint. Requirements should specify constraints that lead naturally to implementation choices during technical planning.

Downstream flow

Requirements constrain technical planning. Technical planning constrains design. Design constrains tests. Tests constrain implementation. Each phase adds detail while respecting previous constraints.

"Password reset tokens expire after 1 hour" is a requirement. Technical planning decides storage mechanism. Design specifies exact schema and signatures. Tests verify expiration behavior. Implementation makes tests pass.

You don't change the 1-hour expiration during implementation because that decision was validated during requirements. If the requirement is wrong, you go back to requirements, change it there, and flow the change forward. This prevents the expensive rework loop where implementation discoveries trigger architectural changes.

The workflow only functions if requirements are complete. Incomplete requirements defer decisions to implementation, which is expensive iteration territory. Complete requirements make implementation mechanical translation from specification to code.

Write requirements that constrain. Use AI to stress-test them. Iterate until they're unambiguous. The 15 iterations at $0.014 each eliminate the 5 iterations at $0.078 each you'd otherwise do in code.