·

Writing Technical Specs For AI Consumption

Writing Technical Specs For AI Consumption

The quality of your specification is now the quality of your AI output — vague documents produce vague code, and gaps in your spec become bugs in production.

Why Specs Look Different When AI Is Reading Them

Engineers have always written specs primarily for other humans. When a colleague reads a PRD or RFC, they fill in gaps using shared context: they know the codebase, the team conventions, what "fast enough" means, and what the unspoken constraints are. They ask clarifying questions. They bring judgment.

AI coding agents do none of that by default. When you hand an agent a spec, it reads exactly what is written. Implicit knowledge, tribal context, and unstated assumptions are invisible to it. If your spec says "add pagination to the user list," the agent will implement pagination — but it may choose an offset-based approach when your team standardized cursor-based pagination, use a page size of 10 when your UX mandates 25, and expose the raw database ID when your API convention hides internal identifiers.

This is not an AI limitation to work around — it is a forcing function that makes you write better specs. Engineers who have adopted spec-driven AI development consistently report that the discipline of writing AI-readable specs also reduces misalignment with human teammates, speeds up code review, and surfaces design problems before a line of code is written.

The practical shift is this: move from writing specs that communicate intent to writing specs that constrain behavior. Intention is for the reader to infer; constraints are for the reader — human or machine — to execute against.

Learning tip: Before passing any spec to an AI agent, read it as if you have zero knowledge of the codebase and no ability to ask follow-up questions. Every question you would ask is a gap you need to fill.

Three Document Types and What Each Signals to an Agent

The software industry uses several document types, and each serves a different purpose in the AI-assisted workflow. Confusing them leads to the wrong kind of output.

Product Requirements Documents (PRDs) answer "what and why." A PRD describes user-facing behavior, business goals, and success criteria. It is intentionally silent on implementation. When you feed a PRD to an AI agent, you are asking it to infer implementation from intent — which produces unpredictable results unless the PRD is unusually precise. PRDs are best used to generate a first-draft RFC or technical design document, not to drive code generation directly.

Requests for Comment (RFCs) answer "how and what are the tradeoffs." An RFC proposes a technical approach, documents alternatives considered, and explains why the chosen path was selected. For AI consumption, the most important sections are the proposed solution, the constraints it must satisfy, and the explicit list of non-goals. RFCs help agents understand the decision boundary: what is in scope, what is explicitly out of scope, and what constraints cannot be violated. A well-written RFC is a strong input for scaffolding, boilerplate generation, and interface design.

Technical Design Documents (TDDs) answer "how exactly will this be built." A TDD contains the implementation blueprint: data models, API contracts, component interactions, migration steps, error handling strategy, and acceptance criteria. This is the document type most directly suited to driving AI code generation. When a TDD is complete and precise, an agent can produce code that integrates cleanly with existing systems rather than greenfielding an imaginary solution.

Understanding which document you are writing — and what role it plays in your AI workflow — prevents the most common mistake: handing a PRD to an agent and expecting TDD-quality output.

Learning tip: Map your documents to a pipeline. Use a PRD as input to generate a TDD outline with AI assistance, then refine that TDD yourself before using it to generate actual code. Each document type feeds the next.

What Makes a Spec AI-Readable

Five properties separate specs that produce accurate AI output from those that produce plausible-but-wrong output.

Explicit scope. State exactly what the feature includes and, equally important, what it does not include. Non-goals are not a formality — they are instructions to the agent to stop. Without them, agents will helpfully extend features into adjacent functionality that was out of scope, duplicating or conflicting with existing code.

Concrete context. Name the actual files, functions, APIs, and data structures the agent will interact with. "The user service" is not context. "/services/user/UserService.ts, specifically the findById and updateProfile methods" is context. Agents cannot browse your codebase while reading a spec (unless you give them that tool), so the spec must carry the context.

Stated constraints. List every technical constraint explicitly: language version, framework version, performance budgets, security requirements, database transaction behavior, backward compatibility requirements, and coding conventions. If your team requires that all database queries go through the repository layer, say so. If a response must complete in under 200ms at p99, write that number down.

Acceptance criteria. Write these as verifiable conditions, not descriptions of desired behavior. "The endpoint should be fast" is not an acceptance criterion. "The endpoint must return HTTP 200 within 300ms for payloads under 1MB, measured with the existing load test suite in /tests/perf/" is an acceptance criterion. Agents use acceptance criteria to self-evaluate generated code.

Edge cases and error handling. Most spec bugs live in edge cases. If a user uploads a zero-byte file, what happens? If the third-party API times out, does the operation fail silently or surface an error to the user? If two concurrent requests try to reserve the same resource, which wins? Explicit edge case documentation prevents agents from making arbitrary choices in these scenarios — choices that are often inconsistent with how you handle similar cases elsewhere.

Learning tip: Write acceptance criteria in the format "Given [context], when [action], then [outcome]." This format is precise enough for agents to reason about and also serves as the basis for automated test cases.

Common Spec Anti-Patterns That Break AI Output

Passive voice and agent ambiguity. "The data should be validated" leaves open who validates it, when, where in the call stack, and what happens on validation failure. Rewrite as: "The createOrder handler must validate the request payload against the OrderSchema before calling the service layer. Invalid payloads must return HTTP 400 with a structured error body conforming to the ApiError type defined in /types/api.ts."

Implicit assumptions about existing behavior. Specs written for human readers often omit context that is "obvious" to anyone who knows the codebase. "Extend the existing search functionality" assumes the agent knows how existing search works, what its inputs and outputs are, and where it lives. Always link to or inline the relevant existing contracts.

Underspecified edge cases. Any conditional in your feature description that is not paired with explicit handling instructions is an underspecified edge case. Review every "if," "when," "unless," and "in case" in your spec and confirm that each has an explicit outcome.

Contradictory requirements. Specs written collaboratively often accumulate contradictions. "The feature must work offline" combined with "all actions must be logged to the central audit service" is a contradiction that a human reviewer would flag. Agents often choose one requirement arbitrarily and satisfy the other incompletely.

Version-agnostic technology references. "Use Redis for caching" without specifying the client library, version, connection pool configuration, and serialization format leaves four decisions to the agent. Each decision is a potential divergence from your standards.

Learning tip: Run a "contradiction check" pass on every spec before sending it to an agent. Look for pairs of requirements that could conflict under edge conditions, and add explicit resolution rules for each pair you find.

Hands-On: Writing a TDD That Drives Clean AI Code Generation

This exercise walks through transforming a weak spec into an AI-ready Technical Design Document for a realistic backend feature: adding rate limiting to a public API endpoint.

Step 1: Start with the vague requirement

You have been given this requirement: "Add rate limiting to the public API to prevent abuse."

Before writing anything, list every implicit assumption in that statement. Typical gaps: which endpoints, what limit, per what time window, keyed on what (IP, user, API key), what response when exceeded, where the limit state is stored, how it handles distributed deployments, and what the monitoring story is.

Step 2: Draft the scope section

Write explicit scope and non-goals before any implementation details.

I am writing a Technical Design Document for adding rate limiting to our public API. Help me draft the Scope and Non-Goals sections given the following context:

- We have a Node.js/Express API with approximately 12 public endpoints under /api/v1/public/
- Rate limiting should apply to all endpoints under that prefix
- We use Redis (ioredis client, v5.x) for shared state across our 4-node deployment
- Authentication is via API keys, passed in the X-API-Key header; unauthenticated requests are rejected before reaching these endpoints

Write the Scope section as a bulleted list of what IS included, and the Non-Goals section as a bulleted list of what is explicitly excluded. Be specific enough that an AI coding agent reading this document would not make decisions outside these boundaries.

Expected output: A scope section that names the endpoint prefix, specifies the key dimension (API key), and confirms Redis as the state store. A non-goals section that explicitly excludes things like authenticated internal endpoints, rate limit customization UI, and per-endpoint limits (if those are out of scope).

Step 3: Write the constraints section

Continue the Technical Design Document for API rate limiting. Write the Constraints section given these requirements:

- Limit: 1000 requests per 15-minute sliding window per API key
- Response when limit exceeded: HTTP 429 with Retry-After header (seconds until window resets) and a JSON body: { "error": "rate_limit_exceeded", "retry_after": <seconds> }
- The rate limit check must add no more than 5ms p99 latency to any request
- The Redis key format must be: ratelimit:{apiKey}:{windowStart} where windowStart is Unix timestamp floored to the nearest 15-minute boundary
- TTL on Redis keys: 20 minutes (to handle clock skew)
- The implementation must use a single Redis pipeline per request (no multiple round-trips)

Format these as a numbered list of hard constraints. Each constraint should be verifiable — either it is satisfied or it is not.

Expected output: A numbered constraints list where each item is a testable condition, not a description of intent. The agent can later use this list as a self-evaluation checklist.

Step 4: Write the acceptance criteria

Translate each constraint and scope item into a "Given/When/Then" acceptance criterion. Write these yourself or prompt the AI to generate a first draft from your constraints section, then review and correct each one.

Step 5: Add the data model and interface contracts

Document the exact Redis key structure, the TypeScript types for the middleware function signature, and the Express middleware interface. Name the file path where the implementation should live.

Step 6: Add edge cases explicitly

Document what happens when Redis is unavailable (fail open or fail closed?), when the API key header is malformed, and when two concurrent requests arrive in the same millisecond for the same key.

Step 7: Generate the implementation

Using the following Technical Design Document, implement the rate limiting middleware for our Express API.

[PASTE YOUR COMPLETE TDD HERE]

Implementation requirements:
- Create the file at /src/middleware/rateLimiter.ts
- Export a single Express middleware function named rateLimitMiddleware
- Use the ioredis client instance exported from /src/infrastructure/redis.ts
- Follow the error handling patterns in /src/middleware/errorHandler.ts
- All Redis operations must use a pipeline
- Include JSDoc comments for the exported function

Do not implement anything outside the scope defined in the TDD. If you encounter an ambiguity not covered by the TDD, stop and list the ambiguities rather than making assumptions.

Expected output: A focused implementation that matches the interface contracts, uses the specified Redis key format, handles the documented edge cases, and stops at the defined scope boundary. Because the TDD was explicit, the agent will not invent a token bucket algorithm when you specified sliding window, will not add per-endpoint limits that were in the non-goals, and will not silently fail on Redis unavailability without your explicit direction.

Step 8: Validate against acceptance criteria

Ask the agent to review its own output against the acceptance criteria section of the TDD. Discrepancies surface gaps in either the implementation or the spec.

Learning tip: The instruction "If you encounter an ambiguity not covered by the TDD, stop and list the ambiguities rather than making assumptions" is one of the most valuable instructions you can add to any code-generation prompt. It converts silent failures into explicit feedback loops.

Document Templates

Minimal AI-ready TDD structure:


## Scope
What this document covers (bullet list).

## Non-Goals
What this document explicitly does not cover (bullet list).

## Context
- Relevant existing files, functions, and types (with paths)
- System state before this change
- Dependencies and their versions

## Constraints
Numbered list of hard requirements. Each must be verifiable.

## Data Models and Interface Contracts
Exact types, schemas, and API signatures.

## Implementation Steps
Ordered list of what to build. Each step names the file and function.

## Edge Cases and Error Handling
Explicit outcomes for each non-happy-path scenario.

## Acceptance Criteria
Given/When/Then format. Each criterion is independently verifiable.

Learning tip: Treat your TDD template as a checklist, not a format. Every section you skip is a decision you are delegating to the agent.

Key Takeaways

  • PRDs define what and why; RFCs define how and the tradeoffs; TDDs define the exact implementation blueprint. Use the right document type at each stage of AI-assisted development, and feed each type into the next rather than jumping directly from PRD to code.
  • AI agents execute against what is written, not what is implied. Explicit scope, concrete context, stated constraints, and precise acceptance criteria are not overhead — they are the primary mechanism for getting correct output.
  • Non-goals are instructions to stop. Without them, agents extend features into adjacent functionality, producing code that is technically impressive but functionally out of scope.
  • Common spec anti-patterns — passive voice, implicit assumptions, underspecified edge cases, and contradictory requirements — produce plausible-but-wrong code. A single read-through asking "what would an agent decide here?" catches most of them.
  • The instruction to stop and surface ambiguities rather than assume is one of the highest-leverage additions to any AI code-generation prompt. It converts silent failures into explicit information you can act on.