> "Give me a specification, and I will give you software. Give me a vague idea, and I will give you a vague program."
In This Chapter
- Learning Objectives
- Introduction
- 10.1 From Vague Idea to Precise Specification
- 10.2 Requirements Documents as Prompts
- 10.3 User Story-Driven Development with AI
- 10.4 API-First Specification
- 10.5 Schema-Driven Development
- 10.6 Test-First Prompting
- 10.7 Interface and Contract Specifications
- 10.8 Configuration and Environment Specs
- 10.9 Specification Templates
- 10.10 When Specifications Help vs. Hinder
- Chapter Summary
- Key Terms
Chapter 10: Specification-Driven Prompting
"Give me a specification, and I will give you software. Give me a vague idea, and I will give you a vague program." -- Adapted from classical engineering wisdom
Learning Objectives
By the end of this chapter, you will be able to:
- Analyze the differences between vague prompts and specification-driven prompts and explain why specifications produce more predictable AI-generated code (Bloom's: Analyze)
- Create formal and semi-formal specification documents that AI coding assistants can translate directly into working code (Bloom's: Create)
- Apply requirements documents, user stories, API specifications, database schemas, and test specifications as structured prompts (Bloom's: Apply)
- Evaluate when specification-driven prompting improves outcomes versus when lighter-weight approaches are more appropriate (Bloom's: Evaluate)
- Design specification templates tailored to different project types and development contexts (Bloom's: Create)
- Synthesize multiple specification types into comprehensive prompting strategies for complex projects (Bloom's: Synthesize)
Introduction
In Chapter 8, you learned the fundamentals of prompt engineering -- how to structure prompts, provide context, and guide AI coding assistants toward better outputs. In Chapter 9, you explored context management -- how to feed the right information to AI at the right time. This chapter takes both concepts to their logical conclusion: what happens when you give AI a complete, formal specification instead of a conversational prompt?
The answer, as you will discover, is transformative. When you provide AI with a well-structured specification, the generated code becomes dramatically more predictable, more correct, and more aligned with your actual requirements. The AI stops guessing about edge cases, stops making assumptions about data types, and stops inventing features you never asked for.
Specification-driven prompting is the bridge between casual vibe coding and professional software development. It takes the structured thinking of traditional software engineering -- requirements documents, API contracts, database schemas, test plans -- and repurposes these artifacts as precision instruments for guiding AI code generation.
This chapter will teach you to write specifications in formats that AI understands exceptionally well, show you how each specification type produces specific kinds of improved output, and give you templates you can adapt for your own projects.
10.1 From Vague Idea to Precise Specification
Every software project begins with an idea. The quality of the code AI generates depends heavily on how far you refine that idea before presenting it to your AI assistant. Let us trace the journey from a vague idea to a precise specification through a concrete example.
The Spectrum of Specification Precision
Consider a developer who wants to build a user authentication system. Here is how that idea might be expressed at different levels of precision:
Level 1: The Vague Idea
Build me a login system.
This prompt gives the AI almost no constraints. What kind of login? For a web app, a CLI tool, a mobile app? What authentication method? What happens on failure? The AI will make dozens of assumptions, and many of them will be wrong for your specific context.
Level 2: The Rough Description
Build a user authentication system for a Flask web application.
It should support email/password login with password hashing.
Better. The AI now knows the framework, the authentication method, and that passwords should be hashed. But critical details are still missing: session management, password requirements, rate limiting, error messages, database schema.
Level 3: The Detailed Description
Build a user authentication system for a Flask web application with
the following features:
- Email/password registration and login
- Passwords hashed with bcrypt, minimum 8 characters
- JWT tokens for session management, 24-hour expiry
- Rate limiting: max 5 failed login attempts per 15 minutes
- Email verification on registration
- Password reset via email
- SQLAlchemy with PostgreSQL for user storage
Significantly better. The AI has concrete technical decisions to work with. However, this is still a description, not a specification. It does not define the exact API endpoints, the database schema, the error response formats, or the expected behavior in every edge case.
Level 4: The Formal Specification
# Authentication System Specification
## Data Model
User:
id: UUID, primary key, auto-generated
email: string, unique, max 255 chars, validated format
password_hash: string, bcrypt, not exposed in any API response
is_verified: boolean, default false
created_at: datetime, UTC, auto-set
updated_at: datetime, UTC, auto-updated
failed_login_attempts: integer, default 0
locked_until: datetime, nullable
## API Endpoints
POST /api/auth/register:
request_body:
email: string, required, valid email format
password: string, required, min 8 chars, 1 uppercase, 1 number
success_response: 201
body: { user_id, email, message: "Verification email sent" }
error_responses:
400: Invalid email format or weak password
409: Email already registered
POST /api/auth/login:
request_body:
email: string, required
password: string, required
success_response: 200
body: { access_token, token_type: "bearer", expires_in: 86400 }
error_responses:
401: Invalid credentials
423: Account locked (too many failed attempts)
403: Email not verified
POST /api/auth/logout:
headers: Authorization: Bearer <token>
success_response: 200
error_responses:
401: Invalid or expired token
## Business Rules
- Passwords: minimum 8 characters, at least 1 uppercase, 1 number
- JWT tokens expire after 24 hours
- After 5 failed login attempts within 15 minutes, lock account for 30 minutes
- Verification emails expire after 24 hours
- Password reset tokens expire after 1 hour
## Technical Requirements
- Framework: Flask 3.x
- ORM: SQLAlchemy 2.x
- Database: PostgreSQL 15+
- Password hashing: bcrypt via passlib
- JWT: PyJWT
- Rate limiting: flask-limiter
This is a specification. Every endpoint is defined. Every data type is explicit. Every error case is enumerated. Every business rule is stated. When you give this to an AI coding assistant, the resulting code will be remarkably close to what you actually want.
Key Insight: The time you invest in writing a specification is almost always less than the time you would spend correcting AI-generated code that was based on vague prompts. A 30-minute specification can save hours of back-and-forth iteration.
The Specification Mindset
Thinking in specifications requires a mental shift. Instead of asking "What do I want this to do?" you need to ask:
- What data does this system manage? Define every entity, every field, every type, every constraint.
- What operations does this system support? Define every endpoint, every function, every command.
- What are the inputs and outputs of each operation? Define request formats, response formats, parameter types.
- What can go wrong? Define every error case and how the system should respond.
- What are the rules? Define business logic, validation rules, and behavioral constraints.
- What are the technical constraints? Define frameworks, libraries, versions, and infrastructure.
Callout -- Common Pitfall: Do not confuse a specification with implementation instructions. A specification says what the system should do, not how to code it. "Hash passwords with bcrypt" is a specification. "Create a function called hash_password that imports bcrypt and calls bcrypt.hashpw()" is implementation instruction. Let the AI handle implementation details; you focus on requirements.
When Specifications Emerge Naturally
You do not always need to sit down and write a specification from scratch. Specifications often emerge naturally from:
- Existing API documentation you are reimplementing or integrating with
- Database schemas from an existing system you are extending
- Test suites that define expected behavior
- Wireframes and mockups that define UI requirements
- Business process documents that define workflows
- Regulatory requirements that define compliance constraints
Learning to recognize these existing artifacts as potential AI prompts is one of the most valuable skills in specification-driven development.
10.2 Requirements Documents as Prompts
Traditional software engineering has long used requirements documents to define what a system should do before any code is written. These documents, often dismissed as bureaucratic overhead, turn out to be exceptionally effective prompts for AI coding assistants.
Functional Requirements as Prompts
A functional requirement states what the system must do. When formatted clearly, functional requirements translate almost directly into AI prompts that generate focused, correct code.
Before: Conversational prompt
I need a library management system that lets librarians manage books
and patrons can check books out.
After: Requirements-driven prompt
Implement a library management system with the following functional requirements:
FR-1: The system shall allow librarians to add new books with title,
author, ISBN (validated 13-digit format), publication year,
and quantity available.
FR-2: The system shall allow librarians to update book information
for any field except ISBN.
FR-3: The system shall allow patrons to search books by title
(partial match, case-insensitive), author, or ISBN (exact match).
FR-4: The system shall allow patrons to check out up to 5 books
simultaneously.
FR-5: The system shall prevent checkout if the patron has any
overdue books.
FR-6: The system shall set a due date of 14 days from checkout date.
FR-7: The system shall calculate late fees at $0.25 per day per book.
FR-8: The system shall send email reminders 3 days before due date
and on the due date.
FR-9: The system shall allow librarians to waive late fees with a
reason field (required).
FR-10: The system shall maintain a complete audit log of all
checkouts, returns, and fee transactions.
The difference in AI output quality between these two prompts is substantial. The requirements-driven prompt eliminates ambiguity about search behavior (partial vs. exact match), defines specific business rules (5-book limit, 14-day loan period, $0.25/day fees), and specifies operational requirements (email reminders, audit logs) that the AI would never infer from the vague prompt.
Non-Functional Requirements
Non-functional requirements define quality attributes -- how well the system should perform, not what it should do. These are frequently overlooked in casual prompting but critically important for production code.
Non-Functional Requirements:
NFR-1: Performance
- Book search shall return results within 200ms for databases
up to 100,000 books
- Checkout/return operations shall complete within 500ms
NFR-2: Security
- All passwords shall be hashed using bcrypt with cost factor 12
- API endpoints shall require JWT authentication
- Patron data shall be encrypted at rest
NFR-3: Reliability
- The system shall handle concurrent checkouts without race
conditions (use database-level locking)
- All database operations shall use transactions
NFR-4: Scalability
- The system shall support up to 10,000 concurrent users
- Database queries shall use appropriate indexes
NFR-5: Maintainability
- Code shall follow PEP 8 style guidelines
- All public functions shall have docstrings
- Test coverage shall be at least 80%
Callout -- Pro Tip: Including non-functional requirements in your prompts often causes AI to generate architecturally sound code from the start. Without NFR-1 above, the AI might generate book searches that scan the entire table. With it, the AI is more likely to include database indexing and efficient query patterns.
The MoSCoW Method for Priority
When your requirements are extensive, use the MoSCoW method (Must have, Should have, Could have, Won't have) to help AI prioritize implementation:
Implement the following requirements using MoSCoW prioritization:
MUST HAVE (implement these first, these are critical):
- User registration and authentication
- Book CRUD operations
- Checkout and return functionality
SHOULD HAVE (implement after must-haves are working):
- Search with filters
- Late fee calculation
- Email notifications
COULD HAVE (implement if time allows):
- Book recommendation engine
- Reading history analytics
- Mobile-responsive UI
WON'T HAVE (explicitly excluded from this version):
- Multi-branch library support
- E-book lending
- Social features
This prioritization helps the AI structure its output logically, implementing foundational components first and building additional features on top of them. It also prevents the AI from spending effort on features you explicitly do not want.
Converting Existing Requirements Documents
If your organization already has requirements documents, you can often feed them directly to AI with minimal modification. The key adaptations are:
- Remove organizational boilerplate -- approval signatures, revision history, and document metadata add noise without helping the AI.
- Clarify ambiguous language -- requirements documents often use phrases like "the system should provide appropriate feedback." Replace these with specific behaviors.
- Add technical context -- requirements documents may not specify the tech stack. Add a technical context section.
- Resolve cross-references -- if requirements reference other documents, inline the relevant information.
10.3 User Story-Driven Development with AI
User stories are a lightweight, human-centered way to express requirements. They follow the format: "As a [role], I want [capability], so that [benefit]." When enriched with acceptance criteria, user stories become powerful prompts for AI code generation.
Anatomy of an Effective User Story Prompt
A basic user story is too vague for AI:
As a user, I want to reset my password so that I can regain access
to my account.
An enriched user story with acceptance criteria gives AI everything it needs:
User Story: Password Reset
As a registered user who has forgotten my password,
I want to request a password reset via email,
So that I can regain access to my account without contacting support.
Acceptance Criteria:
1. GIVEN I am on the login page
WHEN I click "Forgot Password"
THEN I see a form asking for my email address
2. GIVEN I enter a registered email address
WHEN I submit the reset request
THEN I receive an email with a reset link within 2 minutes
AND the link contains a unique, cryptographically secure token
AND the link expires after 1 hour
3. GIVEN I click a valid, non-expired reset link
WHEN I enter a new password meeting the requirements
THEN my password is updated
AND all existing sessions are invalidated
AND I am redirected to the login page with a success message
4. GIVEN I click an expired or invalid reset link
WHEN the page loads
THEN I see an error message: "This reset link has expired or
is invalid. Please request a new one."
AND I see a link to request a new reset
5. GIVEN I enter a new password that does not meet requirements
WHEN I submit the form
THEN I see specific validation messages for each unmet requirement
AND the form retains my input (except the password field)
6. GIVEN I enter an unregistered email address
WHEN I submit the reset request
THEN I see the same success message as for registered emails
(to prevent email enumeration attacks)
Technical Notes:
- Reset tokens: 32-byte random, stored as SHA-256 hash in database
- Email delivery: use SendGrid API
- Password requirements: min 8 chars, 1 uppercase, 1 lowercase,
1 number, 1 special character
- Framework: Django 5.x with Django REST Framework
The Gherkin-style acceptance criteria (GIVEN/WHEN/THEN) are particularly effective because they map naturally to test cases. AI assistants recognize this format and often generate both implementation code and corresponding tests.
From Product Backlog to Implementation
A product backlog is an ordered list of user stories. When working with AI, you can feed an entire backlog or a sprint's worth of stories to give the AI context about the whole system while focusing on specific stories.
# Product Context
We are building "BookClub", a web application for neighborhood
book clubs to manage their reading lists, schedule meetings,
and discuss books.
Tech Stack: Python 3.12, FastAPI, SQLAlchemy 2.x, PostgreSQL,
React frontend (separate repo)
# Sprint 3 Backlog (implement in this order)
## Story 3.1: Club Creation (8 points)
As a registered user,
I want to create a new book club,
So that I can organize a reading community.
Acceptance Criteria:
- Club requires: name (3-100 chars), description (max 500 chars),
city, max_members (5-50)
- Creator automatically becomes club admin
- Club gets a unique URL slug generated from name
- Duplicate club names allowed (distinguished by slug)
...
## Story 3.2: Club Discovery (5 points)
As a registered user,
I want to search for book clubs in my city,
So that I can find and join a reading community.
Acceptance Criteria:
- Search by city name (autocomplete from known cities)
- Filter by: has_openings (boolean), genre preferences
- Sort by: newest, most_members, nearest (if location provided)
- Results paginated, 20 per page
- Each result shows: name, description preview (100 chars),
member_count/max_members, city, genres
...
Callout -- Pattern Recognition: Notice how the backlog provides context about the broader system while each story defines specific functionality. This is exactly the kind of layered context management discussed in Chapter 9. The product context helps AI make consistent architectural decisions across stories, while the individual stories provide the precision needed for correct implementation.
Story Mapping for Complex Features
For features that span multiple user interactions, story mapping helps AI understand the flow:
# Feature: Book Club Meeting Management
## User Journey Map:
Step 1: Admin schedules meeting
Story: "As a club admin, I want to schedule a meeting with date,
time, location, and the book to discuss."
Step 2: Members receive notification
Story: "As a club member, I want to be notified when a new meeting
is scheduled so I can plan to attend."
Step 3: Members RSVP
Story: "As a club member, I want to RSVP to meetings (yes/no/maybe)
so the admin knows who is coming."
Step 4: Admin sees attendance
Story: "As a club admin, I want to see RSVP counts and who is
attending so I can plan the meeting space."
Step 5: Meeting occurs (no system interaction)
Step 6: Post-meeting discussion
Story: "As a meeting attendee, I want to post discussion notes
and ratings after the meeting."
Data Flow:
Meeting -> Notification -> RSVP -> Attendance Report -> Discussion
This story map gives the AI a complete picture of the data flow and user journey, leading to code that properly connects the components rather than implementing each story as an isolated feature.
10.4 API-First Specification
API-first development means designing your API contract before writing any implementation code. This approach is extraordinarily effective with AI because API specifications in standard formats (OpenAPI, GraphQL SDL) are highly structured, unambiguous, and well-understood by language models.
OpenAPI Specifications as Prompts
The OpenAPI Specification (formerly Swagger) is the industry standard for describing REST APIs. AI coding assistants understand OpenAPI exceptionally well because their training data contains thousands of OpenAPI documents.
openapi: 3.1.0
info:
title: BookClub API
version: 1.0.0
description: API for managing neighborhood book clubs
paths:
/api/clubs:
get:
summary: List book clubs
operationId: listClubs
parameters:
- name: city
in: query
required: false
schema:
type: string
description: Filter by city name (case-insensitive partial match)
- name: has_openings
in: query
required: false
schema:
type: boolean
description: Filter to clubs with available membership slots
- name: page
in: query
required: false
schema:
type: integer
minimum: 1
default: 1
- name: per_page
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: Paginated list of clubs
content:
application/json:
schema:
type: object
properties:
clubs:
type: array
items:
$ref: '#/components/schemas/ClubSummary'
total:
type: integer
page:
type: integer
per_page:
type: integer
post:
summary: Create a new book club
operationId: createClub
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateClubRequest'
responses:
'201':
description: Club created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Club'
'400':
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: Authentication required
'409':
description: Club slug conflict
components:
schemas:
ClubSummary:
type: object
required: [id, name, city, member_count, max_members]
properties:
id:
type: string
format: uuid
name:
type: string
description_preview:
type: string
maxLength: 100
city:
type: string
member_count:
type: integer
max_members:
type: integer
genres:
type: array
items:
type: string
CreateClubRequest:
type: object
required: [name, city, max_members]
properties:
name:
type: string
minLength: 3
maxLength: 100
description:
type: string
maxLength: 500
city:
type: string
max_members:
type: integer
minimum: 5
maximum: 50
Club:
type: object
required: [id, name, slug, city, max_members, created_at]
properties:
id:
type: string
format: uuid
name:
type: string
slug:
type: string
description:
type: string
city:
type: string
max_members:
type: integer
member_count:
type: integer
created_at:
type: string
format: date-time
admin_id:
type: string
format: uuid
ErrorResponse:
type: object
required: [error, message]
properties:
error:
type: string
message:
type: string
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
When you give an AI this OpenAPI spec along with a prompt like "Implement this API using FastAPI with SQLAlchemy," the resulting code will closely match the specification -- correct endpoint paths, proper request validation, accurate response schemas, and appropriate error handling.
The Prompt Pattern for API-First Development
Here is the recommended pattern for using API specs with AI:
I have the following OpenAPI specification for a BookClub API.
Please implement this specification using:
- Framework: FastAPI
- ORM: SQLAlchemy 2.x with async support
- Database: PostgreSQL
- Authentication: JWT via python-jose
Requirements:
1. Create SQLAlchemy models that match the component schemas
2. Implement all endpoints exactly as specified
3. Include request validation matching the schema constraints
4. Return the exact response formats defined in the spec
5. Implement proper error handling for all defined error responses
6. Use dependency injection for authentication
[paste OpenAPI spec here]
Callout -- Best Practice: When your API spec is large, break it into logical sections and implement one section at a time. Feed the complete spec for context but ask the AI to implement one group of endpoints per prompt. This keeps the AI focused while maintaining awareness of the overall API design.
GraphQL Schema as Specification
For GraphQL APIs, the Schema Definition Language (SDL) serves the same purpose as OpenAPI:
type Query {
clubs(city: String, hasOpenings: Boolean, page: Int = 1,
perPage: Int = 20): ClubConnection!
club(slug: String!): Club
myClubs: [Club!]!
}
type Mutation {
createClub(input: CreateClubInput!): Club!
joinClub(clubId: ID!): Membership!
leaveClub(clubId: ID!): Boolean!
scheduleMeeting(input: ScheduleMeetingInput!): Meeting!
}
type Club {
id: ID!
name: String!
slug: String!
description: String
city: String!
maxMembers: Int!
memberCount: Int!
members: [Membership!]!
meetings: [Meeting!]!
createdAt: DateTime!
admin: User!
}
input CreateClubInput {
name: String! @constraint(minLength: 3, maxLength: 100)
description: String @constraint(maxLength: 500)
city: String!
maxMembers: Int! @constraint(min: 5, max: 50)
}
AI assistants trained on GraphQL schemas can generate resolvers, data loaders, and database queries that align precisely with the schema definition.
10.5 Schema-Driven Development
Database schemas and data models are among the most effective specifications for AI prompting. When the AI knows your exact data model, it generates code that correctly handles relationships, constraints, and queries.
SQL Schema as Specification
A well-defined SQL schema is an unambiguous specification for data storage:
-- Database Schema: BookClub Application
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
avatar_url VARCHAR(500),
city VARCHAR(100),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE clubs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
slug VARCHAR(120) UNIQUE NOT NULL,
description TEXT,
city VARCHAR(100) NOT NULL,
max_members INTEGER NOT NULL CHECK (max_members BETWEEN 5 AND 50),
admin_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE memberships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
club_id UUID NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL DEFAULT 'member'
CHECK (role IN ('admin', 'moderator', 'member')),
joined_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, club_id)
);
CREATE TABLE meetings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
club_id UUID NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
title VARCHAR(200) NOT NULL,
description TEXT,
meeting_date TIMESTAMP WITH TIME ZONE NOT NULL,
location VARCHAR(300),
book_title VARCHAR(300),
book_author VARCHAR(200),
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE rsvps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
meeting_id UUID NOT NULL REFERENCES meetings(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status VARCHAR(10) NOT NULL CHECK (status IN ('yes', 'no', 'maybe')),
responded_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(meeting_id, user_id)
);
-- Indexes for common query patterns
CREATE INDEX idx_clubs_city ON clubs(city);
CREATE INDEX idx_clubs_slug ON clubs(slug);
CREATE INDEX idx_memberships_user ON memberships(user_id);
CREATE INDEX idx_memberships_club ON memberships(club_id);
CREATE INDEX idx_meetings_club_date ON meetings(club_id, meeting_date);
CREATE INDEX idx_rsvps_meeting ON rsvps(meeting_id);
When you provide this schema to an AI with a prompt like "Generate SQLAlchemy models and a FastAPI CRUD layer for this database schema," the AI produces models that exactly match your constraints, relationships, and indexes.
Pydantic Models as Schema Specifications
For Python projects, Pydantic models serve as both schema definition and runtime validation. They are excellent specifications because they combine type information, validation rules, and documentation in one place:
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime
from uuid import UUID
from enum import Enum
class MemberRole(str, Enum):
ADMIN = "admin"
MODERATOR = "moderator"
MEMBER = "member"
class RSVPStatus(str, Enum):
YES = "yes"
NO = "no"
MAYBE = "maybe"
class CreateClubRequest(BaseModel):
"""Request schema for creating a new book club."""
name: str = Field(
..., min_length=3, max_length=100,
description="Club name, must be unique within a city"
)
description: str | None = Field(
None, max_length=500,
description="Optional description of the club"
)
city: str = Field(
..., min_length=2, max_length=100,
description="City where the club is based"
)
max_members: int = Field(
..., ge=5, le=50,
description="Maximum number of members (5-50)"
)
class ClubResponse(BaseModel):
"""Response schema for a book club."""
id: UUID
name: str
slug: str
description: str | None
city: str
max_members: int
member_count: int
created_at: datetime
admin_id: UUID
model_config = {"from_attributes": True}
Callout -- Why This Works: Pydantic models are a form of executable specification. They do not just describe the data -- they enforce it at runtime. When AI sees a Pydantic model, it understands both the data structure and the validation rules, and generates code that leverages Pydantic's validation automatically.
JSON Schema for Cross-Language Specifications
When your specification needs to be language-agnostic, JSON Schema is the standard:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "BookClub Configuration",
"type": "object",
"required": ["name", "city", "max_members"],
"properties": {
"name": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Club name"
},
"description": {
"type": "string",
"maxLength": 500
},
"city": {
"type": "string"
},
"max_members": {
"type": "integer",
"minimum": 5,
"maximum": 50
},
"genres": {
"type": "array",
"items": {
"type": "string",
"enum": ["fiction", "non-fiction", "mystery", "sci-fi",
"romance", "biography", "history", "science"]
},
"uniqueItems": true
}
}
}
10.6 Test-First Prompting
Test-first prompting inverts the typical AI coding workflow. Instead of describing what you want and asking AI to write code, you write the tests first and ask the AI to write code that passes them. This is AI-assisted Test-Driven Development (TDD), and it produces remarkably high-quality results.
The Test-First Workflow
The traditional TDD cycle is Red-Green-Refactor: write a failing test, write code to pass it, then refactor. With AI, the workflow becomes:
- You write the tests -- defining expected behavior precisely
- AI writes the implementation -- generating code to pass your tests
- You run the tests -- verifying the AI's implementation
- You refactor together -- improving the code while keeping tests green
Writing Tests as Specifications
Tests are the most precise form of specification because they define exact inputs, exact outputs, and exact behavior. There is no ambiguity in a test:
import pytest
from datetime import datetime, timedelta
from decimal import Decimal
class TestLateFeesCalculation:
"""Tests for the library late fee calculation system.
Business Rules:
- Standard fee: $0.25 per day per book
- Maximum fee: $25.00 per book
- Children's books: $0.10 per day, max $10.00
- Grace period: 1 day (no fee if returned 1 day late)
- Weekends and holidays do not count as late days
"""
def test_no_fee_when_returned_on_time(self, calculator):
due_date = datetime(2025, 6, 15)
return_date = datetime(2025, 6, 15)
fee = calculator.calculate_fee(
due_date=due_date,
return_date=return_date,
book_type="standard"
)
assert fee == Decimal("0.00")
def test_no_fee_within_grace_period(self, calculator):
due_date = datetime(2025, 6, 15) # Sunday
return_date = datetime(2025, 6, 16) # Monday, 1 day late
fee = calculator.calculate_fee(
due_date=due_date,
return_date=return_date,
book_type="standard"
)
assert fee == Decimal("0.00")
def test_standard_fee_after_grace_period(self, calculator):
due_date = datetime(2025, 6, 10) # Tuesday
return_date = datetime(2025, 6, 13) # Friday, 3 days late
# 3 days late - 1 grace day = 2 billable days
fee = calculator.calculate_fee(
due_date=due_date,
return_date=return_date,
book_type="standard"
)
assert fee == Decimal("0.50")
def test_weekends_excluded(self, calculator):
due_date = datetime(2025, 6, 13) # Friday
return_date = datetime(2025, 6, 18) # Wednesday, 5 calendar days
# Only Mon, Tue, Wed count = 3 late days - 1 grace = 2 billable
fee = calculator.calculate_fee(
due_date=due_date,
return_date=return_date,
book_type="standard"
)
assert fee == Decimal("0.50")
def test_maximum_fee_cap(self, calculator):
due_date = datetime(2025, 1, 1)
return_date = datetime(2025, 12, 31)
fee = calculator.calculate_fee(
due_date=due_date,
return_date=return_date,
book_type="standard"
)
assert fee == Decimal("25.00")
def test_childrens_book_reduced_rate(self, calculator):
due_date = datetime(2025, 6, 10) # Tuesday
return_date = datetime(2025, 6, 13) # Friday, 3 days late
# 2 billable days * $0.10 = $0.20
fee = calculator.calculate_fee(
due_date=due_date,
return_date=return_date,
book_type="children"
)
assert fee == Decimal("0.20")
def test_childrens_book_max_fee(self, calculator):
due_date = datetime(2025, 1, 1)
return_date = datetime(2025, 12, 31)
fee = calculator.calculate_fee(
due_date=due_date,
return_date=return_date,
book_type="children"
)
assert fee == Decimal("10.00")
def test_early_return_no_fee(self, calculator):
due_date = datetime(2025, 6, 15)
return_date = datetime(2025, 6, 10)
fee = calculator.calculate_fee(
due_date=due_date,
return_date=return_date,
book_type="standard"
)
assert fee == Decimal("0.00")
Now you can give this test file to AI with a simple prompt:
Here are my tests for a late fee calculation system. Please write
the LateFeeCalculator class that passes all these tests. Include
the pytest fixture for 'calculator'. Follow the business rules
documented in the test class docstring.
The AI has no room for misinterpretation. The tests define exact inputs, exact outputs, and exact edge cases. The resulting implementation will be correct -- or it will not pass the tests, and you will know immediately.
Benefits of Test-First Prompting
Test-first prompting has several advantages over implementation-first approaches:
-
Unambiguous specification: Tests define behavior precisely. There is no room for the AI to misinterpret "calculate late fees" when the exact calculation is demonstrated through examples.
-
Built-in verification: You can immediately verify the AI's output by running the tests. No manual inspection required.
-
Edge case coverage: Writing tests forces you to think about edge cases (early returns, maximum fees, weekends) that you might forget to mention in a natural language prompt.
-
Regression protection: The tests remain valuable long after the initial implementation, protecting against bugs during future modifications.
-
Documentation: The tests serve as living documentation of expected behavior.
Callout -- From Chapter 8: Remember the PRECISE framework from Chapter 8? Test-first prompting is the ultimate expression of the "E" (Examples) element. Instead of providing one or two examples in your prompt, you provide a comprehensive set of examples in executable form.
Property-Based Test Specifications
For mathematical or algorithmic functions, property-based tests can be even more powerful than example-based tests:
from hypothesis import given, strategies as st
class TestSortingAlgorithm:
"""Properties that any correct sorting implementation must satisfy."""
@given(st.lists(st.integers()))
def test_output_length_equals_input_length(self, input_list):
result = sort_function(input_list)
assert len(result) == len(input_list)
@given(st.lists(st.integers()))
def test_output_is_ordered(self, input_list):
result = sort_function(input_list)
for i in range(len(result) - 1):
assert result[i] <= result[i + 1]
@given(st.lists(st.integers()))
def test_output_is_permutation_of_input(self, input_list):
result = sort_function(input_list)
assert sorted(result) == sorted(input_list)
@given(st.lists(st.integers(), min_size=0, max_size=0))
def test_empty_list(self, input_list):
result = sort_function(input_list)
assert result == []
@given(st.lists(st.integers(), min_size=1, max_size=1))
def test_single_element(self, input_list):
result = sort_function(input_list)
assert result == input_list
These property-based tests do not specify a particular sorting algorithm; they specify the properties that any correct sort must satisfy. This gives the AI freedom to choose an implementation approach while constraining the correctness.
10.7 Interface and Contract Specifications
Interface specifications define the boundaries between components. When you specify interfaces before implementation, you give the AI a clear picture of how components should interact, leading to more modular, maintainable code.
Python Protocol and Abstract Base Class Specifications
Python's Protocol classes and ABCs (Abstract Base Classes) are natural specification tools:
from typing import Protocol, runtime_checkable
from datetime import datetime
from uuid import UUID
@runtime_checkable
class BookRepository(Protocol):
"""Interface for book data access.
Any class implementing this protocol must support these operations.
Implementations may use SQL databases, NoSQL stores, or in-memory
storage.
"""
async def get_by_id(self, book_id: UUID) -> Book | None:
"""Retrieve a book by its unique identifier.
Returns None if no book exists with the given ID.
Must complete within 100ms for indexed lookups.
"""
...
async def search(
self,
query: str,
filters: BookFilters | None = None,
page: int = 1,
per_page: int = 20
) -> PaginatedResult[Book]:
"""Search books by title or author.
Query performs case-insensitive partial matching.
Results ordered by relevance, then by title alphabetically.
Must complete within 200ms for databases up to 100k books.
"""
...
async def create(self, book: CreateBookRequest) -> Book:
"""Create a new book record.
Raises DuplicateISBNError if ISBN already exists.
Raises ValidationError if required fields are missing.
"""
...
async def update(
self, book_id: UUID, updates: UpdateBookRequest
) -> Book:
"""Update an existing book record.
Raises NotFoundError if book does not exist.
Raises ValidationError if updates violate constraints.
ISBN cannot be modified after creation.
"""
...
async def delete(self, book_id: UUID) -> bool:
"""Delete a book record.
Returns True if deleted, False if not found.
Raises IntegrityError if book has active checkouts.
"""
...
This interface specification tells the AI exactly what methods to implement, what parameters they take, what they return, and what errors they raise. You can then prompt:
Implement the BookRepository protocol using SQLAlchemy 2.x async
with PostgreSQL. Create a class called PostgresBookRepository.
Ensure all performance requirements in the docstrings are met
through appropriate indexing and query optimization.
Design by Contract
Design by Contract (DbC) adds preconditions, postconditions, and invariants to your specifications:
class ShoppingCart:
"""Shopping cart with contract specifications.
Invariants:
- total_price is always >= 0
- total_price always equals sum of (item.price * item.quantity)
for all items
- item_count always equals sum of item.quantity for all items
- No item can have quantity <= 0
"""
def add_item(self, product_id: str, quantity: int,
unit_price: Decimal) -> None:
"""Add an item to the cart.
Preconditions:
- quantity > 0
- unit_price >= 0
- product_id is non-empty string
Postconditions:
- If product was already in cart, its quantity increases
by the given amount
- If product was not in cart, it is added with given
quantity and price
- total_price increases by (quantity * unit_price)
- item_count increases by quantity
"""
...
def remove_item(self, product_id: str) -> None:
"""Remove an item entirely from the cart.
Preconditions:
- product_id exists in the cart
Postconditions:
- The item is no longer in the cart
- total_price decreases by (removed_item.quantity *
removed_item.unit_price)
- item_count decreases by removed_item.quantity
Raises:
- ItemNotFoundError if product_id not in cart
"""
...
def apply_discount(self, code: str) -> Decimal:
"""Apply a discount code to the cart.
Preconditions:
- code is a valid, non-expired discount code
- No discount has already been applied
Postconditions:
- total_price is reduced by the discount amount
- total_price is never negative (floor at 0.00)
- The discount code is marked as used for this cart
- Returns the discount amount applied
Raises:
- InvalidDiscountError if code is invalid or expired
- DiscountAlreadyAppliedError if a discount was already used
"""
...
Contracts give AI precise behavioral requirements that go beyond type signatures. The AI can use preconditions to generate input validation, postconditions to generate assertions and state updates, and invariants to generate consistency checks.
Callout -- Integration with Testing: Contract specifications naturally produce test cases. Each precondition suggests a negative test (what happens when the precondition is violated), and each postcondition suggests a positive test (verify the postcondition holds after the operation).
10.8 Configuration and Environment Specs
Configuration specifications define how your application should behave in different environments. These are often overlooked in AI prompting but can prevent a whole class of deployment issues.
Environment Configuration Specification
# Environment Configuration Specification
environments:
development:
database:
host: localhost
port: 5432
name: bookclub_dev
pool_size: 5
echo_sql: true
server:
host: 0.0.0.0
port: 8000
reload: true
debug: true
auth:
jwt_secret: dev-secret-key-not-for-production
token_expiry_hours: 72
require_email_verification: false
logging:
level: DEBUG
format: verbose
cors:
allowed_origins: ["http://localhost:3000"]
testing:
database:
host: localhost
port: 5432
name: bookclub_test
pool_size: 2
echo_sql: false
server:
host: 127.0.0.1
port: 8001
reload: false
debug: false
auth:
jwt_secret: test-secret-key
token_expiry_hours: 1
require_email_verification: false
logging:
level: WARNING
format: json
production:
database:
host: ${DB_HOST}
port: ${DB_PORT}
name: ${DB_NAME}
pool_size: 20
echo_sql: false
ssl_mode: require
server:
host: 0.0.0.0
port: ${PORT}
reload: false
debug: false
workers: 4
auth:
jwt_secret: ${JWT_SECRET}
token_expiry_hours: 24
require_email_verification: true
logging:
level: INFO
format: json
cors:
allowed_origins: ${ALLOWED_ORIGINS}
When you give this to AI with a prompt like "Create a configuration management module that loads these settings using Pydantic Settings," the AI generates code that handles environment variables, defaults, and validation correctly.
Docker and Infrastructure Specifications
Infrastructure specifications are also effective prompts:
# Docker Compose Specification
services:
api:
build: ./api
ports: ["8000:8000"]
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/bookclub
- JWT_SECRET=${JWT_SECRET}
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
environment:
- POSTGRES_DB=bookclub
- POSTGRES_USER=user
- POSTGRES_PASSWORD=${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d bookclub"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
ports: ["6379:6379"]
volumes:
pgdata:
Callout -- Environment Awareness: When your AI prompt includes infrastructure specifications, the generated application code often handles environment-specific concerns more gracefully -- connection pooling, health checks, graceful shutdown, and configuration via environment variables.
10.9 Specification Templates
Having reusable specification templates accelerates your workflow. Instead of writing specifications from scratch for each project, you adapt templates to your specific needs.
Microservice Specification Template
# Microservice Specification: [Service Name]
## Overview
- **Purpose**: [One-sentence description]
- **Domain**: [Bounded context in your architecture]
- **Team**: [Owning team]
## Technical Stack
- Language: [e.g., Python 3.12]
- Framework: [e.g., FastAPI]
- Database: [e.g., PostgreSQL 16]
- Cache: [e.g., Redis 7]
- Message Queue: [e.g., RabbitMQ / Kafka topic]
## Data Model
[SQL schema or Pydantic models for all entities]
## API Endpoints
[OpenAPI spec or endpoint list with request/response formats]
## Events Published
[List of events this service publishes to the message queue]
- Event: [name]
Payload: [schema]
Trigger: [when this event is published]
## Events Consumed
[List of events this service subscribes to]
- Event: [name]
Source: [publishing service]
Handler: [what this service does when receiving this event]
## External Dependencies
[APIs, services, or resources this service depends on]
## Configuration
[Environment variables and their purposes]
## Health Checks
- Readiness: [what must be true for the service to receive traffic]
- Liveness: [what is checked to ensure the service is running]
## Non-Functional Requirements
- Latency: [p50, p95, p99 targets]
- Throughput: [requests per second]
- Availability: [target percentage]
CLI Tool Specification Template
# CLI Tool Specification: [Tool Name]
## Purpose
[What problem does this tool solve?]
## Installation
[How is it installed? pip, brew, binary download?]
## Commands
### [command-name]
Description: [what it does]
Usage: tool-name command-name [options] <arguments>
Arguments:
<arg1> [description] (required)
<arg2> [description] (optional, default: X)
Options:
-o, --option [description] (default: X)
-v, --verbose [description]
--format [output format: json|table|csv] (default: table)
Examples:
$ tool-name command-name "input" --format json
$ tool-name command-name --verbose
Exit Codes:
0: Success
1: General error
2: Invalid input
3: Network error
### [next-command]
...
## Configuration File
Location: ~/.tool-name/config.yaml
Format:
default_format: table
api_key: ${TOOL_API_KEY}
timeout: 30
## Output Formats
[Define what each output format looks like]
CRUD Application Specification Template
# CRUD Application: [App Name]
## Entities
### [Entity Name]
Fields:
- id: UUID, auto-generated, primary key
- [field]: [type], [constraints], [description]
- created_at: datetime, auto-set
- updated_at: datetime, auto-updated
Relationships:
- [relationship description]
Validation Rules:
- [rule 1]
- [rule 2]
### [Next Entity]
...
## Operations per Entity
### Create
- Required fields: [list]
- Optional fields: [list]
- Auto-generated fields: [list]
- Side effects: [e.g., send notification, update cache]
### Read (Single)
- Lookup by: [id, slug, etc.]
- Include related: [related entities to eager-load]
### Read (List)
- Filters: [filterable fields and operators]
- Sort options: [sortable fields, default sort]
- Pagination: [style -- offset or cursor, default page size]
### Update
- Updatable fields: [list]
- Immutable fields: [list]
- Side effects: [e.g., clear cache, notify subscribers]
### Delete
- Soft delete or hard delete?
- Cascade behavior: [what happens to related records]
- Authorization: [who can delete]
## Authorization Rules
- [Role]: [allowed operations]
- [Role]: [allowed operations]
Callout -- Template Customization: These templates are starting points, not rigid forms. Remove sections that do not apply to your project and add sections for domain-specific concerns. The goal is to prompt your own thinking as much as to prompt the AI.
10.10 When Specifications Help vs. Hinder
Specification-driven prompting is powerful, but it is not always the right approach. Understanding when to invest in detailed specifications and when to use lighter-weight prompting is a judgment skill that improves with experience.
When Specifications Help Most
Complex business logic: When the code must implement intricate rules with many edge cases, a specification prevents the AI from missing cases.
# Without specification: "Calculate shipping costs"
# (AI guesses at zones, weights, surcharges)
# With specification:
Shipping Cost Rules:
- Zone A (< 100 miles): base $5.99
- Zone B (100-500 miles): base $9.99
- Zone C (500+ miles): base $14.99
- Weight surcharge: +$1.50 per pound over 5 lbs
- Oversize surcharge: +$8.00 if any dimension > 24 inches
- Hazmat surcharge: +$12.00
- Free shipping if order total > $75 (before surcharges)
- Express: 2x base rate
- Saturday delivery: +$15.00 (only Zone A and B)
Multi-component systems: When code must interact with other components, specifications ensure the interfaces match.
Regulatory or compliance requirements: When code must meet specific standards, specifications document exactly what is required.
Team projects: When multiple developers (or multiple AI sessions) work on the same system, specifications ensure consistency.
APIs consumed by others: When other developers will use your API, getting the contract right matters more than implementation speed.
When Specifications Hinder
Exploration and prototyping: When you are not sure what you want yet, writing a detailed specification is premature. Use conversational prompting to explore ideas, then formalize what works.
# Good for exploration:
"What are some approaches to building a recommendation engine
for a book club app? Show me a simple prototype of each."
# Premature specification:
"Build a recommendation engine with these exact weights:
genre_match: 0.35, author_match: 0.25, ..."
# (You don't know the right weights yet)
Simple, well-understood tasks: If you need a function to parse a CSV file, a conversational prompt is often sufficient. Over-specifying trivial tasks wastes time.
Rapidly changing requirements: If requirements are changing daily, maintaining detailed specifications creates overhead. Use lighter-weight user stories until requirements stabilize.
Learning and experimentation: When your goal is to learn how something works, a specification constrains the AI from showing you alternative approaches. Conversational exploration is more educational.
The Specification Spectrum
In practice, most projects benefit from a mix of approaches:
| Component | Specification Level | Why |
|---|---|---|
| Database schema | High (formal schema) | Data structures are foundational; errors here propagate everywhere |
| API endpoints | High (OpenAPI spec) | APIs are contracts; changes break consumers |
| Business logic | Medium-High (requirements + tests) | Complex rules need precision; tests catch errors |
| UI components | Medium (user stories + mockups) | UI is iterative; over-specifying limits exploration |
| Utility functions | Low (conversational) | Simple, well-understood patterns |
| Prototypes | Minimal (exploratory) | Goal is to learn, not to build |
Incremental Specification
You do not need to write a complete specification before starting. A practical approach is incremental specification:
- Start with a rough description to generate a prototype
- Identify the parts that matter most (data model, API contracts, critical business logic)
- Write formal specifications for those parts and regenerate them
- Keep other parts as conversational prompts until they stabilize
- Formalize more over time as the system matures
This approach gives you the benefits of specification-driven development where they matter most while avoiding the overhead where they do not.
Callout -- The 80/20 Rule of Specifications: In most projects, 20% of the code contains 80% of the complexity. Focus your specification effort on that 20%. The authentication system, the payment processing, the complex business rules -- these deserve detailed specs. The "list all items" endpoint does not need a page-long specification.
Signs You Need More Specification
Watch for these indicators that your prompts need more formality:
- The AI keeps getting the same thing wrong after multiple iterations -- you have an ambiguity that only a specification can resolve
- Generated code handles some edge cases but misses others -- you need to enumerate all cases explicitly
- Two parts of the system do not work together -- you need interface specifications
- You keep correcting data types or formats -- you need schema specifications
- The AI generates extra features you did not want -- you need explicit scope boundaries
Signs You Have Too Much Specification
Conversely, watch for these signs of over-specification:
- You spend more time writing specs than the AI would spend generating code -- simplify
- The specification is so long the AI truncates or ignores parts -- break it into focused sections (as covered in Chapter 9's context management)
- You are specifying implementation details instead of requirements -- step back and focus on what, not how
- Requirements are still changing and you keep updating the specification -- use a lighter format until things stabilize
Chapter Summary
Specification-driven prompting represents a maturation in how we work with AI coding assistants. By investing time in clear, structured specifications, we dramatically improve the quality, predictability, and correctness of generated code.
The key insight of this chapter is that specifications are not bureaucratic overhead -- they are precision instruments for communicating with AI. Every format we explored -- requirements documents, user stories, OpenAPI specs, database schemas, test suites, interface definitions, and configuration specifications -- gives AI a different kind of precision:
- Requirements documents eliminate ambiguity about what the system should do
- User stories with acceptance criteria define behavior from the user's perspective
- API specifications define contracts that generated code must conform to
- Database schemas ensure correct data modeling and relationships
- Test specifications provide the most precise behavioral definition possible
- Interface contracts ensure components work together correctly
- Configuration specs prevent environment-related issues
The art lies in knowing when each approach is appropriate and how much formality to apply. Use the spectrum: formal specifications for complex, critical, or shared components; lighter approaches for simple, exploratory, or rapidly changing work.
In Chapter 11, you will learn about iterative refinement -- how to take AI-generated code and progressively improve it through focused follow-up prompts. The specifications you learned to write in this chapter will serve as the foundation against which you measure each iteration's progress.
Key Terms
- Specification-driven prompting: Providing AI with formal or semi-formal specifications instead of conversational descriptions
- Functional requirements: Statements of what a system must do
- Non-functional requirements: Quality attributes (performance, security, reliability)
- Acceptance criteria: Specific conditions that must be met for a user story to be considered complete
- OpenAPI specification: Industry standard format for describing REST APIs
- Schema-driven development: Using data schemas as the primary specification for code generation
- Test-first prompting: Writing tests before asking AI to write implementation code
- Design by contract: Specifying preconditions, postconditions, and invariants for operations
- Interface specification: Defining the boundaries and contracts between system components
- MoSCoW method: Prioritization technique using Must, Should, Could, and Won't categories
- Incremental specification: Progressively adding formality to specifications as the system matures
Related Reading
Explore this topic in other books
Vibe Coding Prompt Engineering Fundamentals Vibe Coding Advanced Prompting Techniques AI Engineering Prompt Engineering