Chapter 5: Spec-Anchored Development
Learning Objectives
By the end of this chapter, you will be able to:
- Define spec-anchored development and explain how it solves the drift problem
- Implement three types of executable specifications: OpenAPI contracts, BDD scenarios, and database schema definitions
- Set up automated validation that fails the build when code diverges from specification
- Build a spec-anchored API endpoint with automated contract testing
- Evaluate the tradeoffs of spec-anchored vs. spec-first development
From Written Specs to Executable Specs
Spec-first development (Chapter 4) introduced a critical discipline: writing specifications before code. But we identified a weakness — specifications drift from code because there's no automated mechanism to keep them aligned.
Spec-anchored development solves this by making specifications executable. Instead of specifications that humans read and manually verify, spec-anchored specifications are machine-readable artifacts that automated tools validate against the running code.
Spec-First:
Spec (Markdown) → Human reads → Human implements → Human validates
⚠️ Drift possible at every human step
Spec-Anchored:
Spec (Machine-readable) → Tools validate → Build fails on divergence
✅ Drift detected automatically
The key insight: if your specification can be checked by a machine, drift becomes a build failure instead of a latent bug.
Three Pillars of Spec-Anchored Development
Pillar 1: API Contracts (OpenAPI / AsyncAPI)
An OpenAPI specification defines your API's surface area — endpoints, request/response schemas, authentication requirements, error formats — in a machine-readable format.
# openapi.yaml — The specification
openapi: 3.1.0
info:
title: Bookmark API
version: 1.0.0
paths:
/api/bookmarks:
post:
summary: Bookmark an article
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [article_id]
properties:
article_id:
type: string
format: uuid
responses:
'201':
description: Bookmark created
content:
application/json:
schema:
$ref: '#/components/schemas/Bookmark'
'401':
description: Not authenticated
'409':
description: Article already bookmarked
get:
summary: List user bookmarks
security:
- bearerAuth: []
parameters:
- name: page
in: query
schema:
type: integer
minimum: 1
default: 1
- name: per_page
in: query
schema:
type: integer
minimum: 1
maximum: 50
default: 20
responses:
'200':
description: Paginated bookmark list
content:
application/json:
schema:
$ref: '#/components/schemas/BookmarkList'
This specification is both human-readable documentation and a machine-enforceable contract. Tools like Specmatic, Prism, and openapi-diff can:
- Validate that your API implementation matches the spec
- Detect breaking changes when the spec is modified
- Generate mock servers from the spec for frontend development
- Generate client SDKs from the spec
Pillar 2: Behavioral Specifications (BDD)
Behavioral specifications define system behavior in a structured, human-readable format that translates directly to automated tests:
# bookmarks.feature — The specification
Feature: Article Bookmarks
Users can save articles for later reading
Background:
Given I am logged in as "alice@example.com"
And the article "Introduction to SDD" exists
Scenario: Bookmark an article
When I bookmark the article "Introduction to SDD"
Then the article should appear in my saved articles
And the bookmark icon should show as filled
Scenario: Remove a bookmark
Given I have bookmarked "Introduction to SDD"
When I remove the bookmark from "Introduction to SDD"
Then the article should not appear in my saved articles
And the bookmark icon should show as unfilled
Scenario: Bookmark limit enforcement
Given I have 500 bookmarked articles
When I try to bookmark a new article
Then I should see the message "Bookmark limit reached (500)"
And the article should not be bookmarked
Scenario: Bookmark privacy
Given "bob@example.com" has bookmarked "Introduction to SDD"
When I view my saved articles
Then I should not see "Introduction to SDD"
Each scenario is:
- A specification: It defines expected behavior
- A test: It can be executed by BDD frameworks (Cucumber, Behave, Playwright BDD)
- Documentation: It's readable by non-technical stakeholders
When the system's behavior diverges from the specification, the BDD tests fail. Drift is impossible.
Pillar 3: Schema Definitions
Database schemas, data models, and type definitions serve as specifications for data structure:
-- schema.sql — The specification
CREATE TABLE bookmarks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, article_id) -- Enforces: cannot bookmark same article twice
);
CREATE INDEX idx_bookmarks_user_id_created
ON bookmarks(user_id, created_at DESC); -- Supports: sorted by recency
-- Constraint: max 500 bookmarks per user
CREATE OR REPLACE FUNCTION check_bookmark_limit()
RETURNS TRIGGER AS $$
BEGIN
IF (SELECT COUNT(*) FROM bookmarks WHERE user_id = NEW.user_id) >= 500 THEN
RAISE EXCEPTION 'Bookmark limit reached (500)';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER bookmark_limit_trigger
BEFORE INSERT ON bookmarks
FOR EACH ROW EXECUTE FUNCTION check_bookmark_limit();
The schema itself enforces specifications: uniqueness constraints prevent duplicate bookmarks, foreign key constraints enforce referential integrity, and trigger functions enforce business rules.
Tutorial: Building a Spec-Anchored API
Let's build the bookmark API with all three spec anchors in place.
Step 1: Write the OpenAPI Contract First
Create specs/bookmarks/openapi.yaml with the full API contract (endpoints, schemas, error responses). This is your source of truth for the API surface.
Step 2: Write BDD Scenarios
Create specs/bookmarks/bookmarks.feature with behavioral scenarios for every acceptance criterion. These are your source of truth for system behavior.
Step 3: Define the Database Schema
Create specs/bookmarks/schema.sql with the data model, constraints, and indexes. This is your source of truth for data structure.
Step 4: Generate Implementation from Specs
Give all three specifications to your AI assistant:
"Implement the bookmark API based on these three specifications:
- OpenAPI contract (API surface)
- BDD scenarios (behavior)
- Database schema (data model)
The implementation must pass contract validation against the OpenAPI spec and all BDD scenarios. Do not add any behavior not defined in the specs."
Step 5: Set Up Automated Validation
Configure your CI pipeline to validate all three anchors:
# .github/workflows/spec-validation.yml
name: Spec Validation
on: [push, pull_request]
jobs:
contract-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate API against OpenAPI spec
run: npx @stoplight/prism-cli proxy specs/bookmarks/openapi.yaml
bdd-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run BDD scenarios
run: npx cucumber-js specs/bookmarks/*.feature
schema-validation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate schema migrations
run: npm run db:migrate && npm run db:validate-schema
Now if any code change violates the specification, the build fails. Drift is detected automatically.
The Spec-Anchored Feedback Loop
The power of spec-anchored development comes from its feedback loop:
Specification (OpenAPI + BDD + Schema)
↓
Implementation (generated or manual)
↓
Automated Validation (CI pipeline)
↓
┌─ PASS → Deploy
└─ FAIL → Fix implementation OR update specification
(both require conscious decision)
When validation fails, you have exactly two options:
- Fix the implementation to match the specification (the spec was right, the code was wrong)
- Update the specification to match the new requirement (the requirement changed, both spec and code need updating)
There is no third option. You cannot silently let code drift from the specification because the CI pipeline catches it.
This forced decision is the key benefit: every change is either spec-compliant or spec-updating. The gray zone of "code changed but spec didn't" is eliminated.
Tradeoffs: Spec-First vs. Spec-Anchored
| Aspect | Spec-First | Spec-Anchored |
|---|---|---|
| Setup cost | Low (just write Markdown) | Medium (tools, CI pipeline, machine-readable formats) |
| Drift protection | Manual discipline | Automated enforcement |
| Specification format | Natural language (Markdown) | Machine-readable (OpenAPI, Gherkin, SQL) |
| Stakeholder readability | High | Medium (BDD is readable; OpenAPI less so) |
| Feedback speed | Manual review | Instant (CI pipeline) |
| Learning curve | Low | Medium |
| Best for | Small teams, early-stage projects | Growing teams, production systems |
Expert Insight: Most teams should start with spec-first (Chapter 4) and evolve to spec-anchored as the project matures. The transition is natural: your Markdown acceptance criteria become Gherkin scenarios, your informal API descriptions become OpenAPI specs, and your data model notes become schema definitions.
Try With AI
Prompt 1: Generate Contract Tests
"I have an OpenAPI specification for a bookmark API. Generate contract tests that validate every endpoint, request schema, and response schema defined in the spec. The tests should fail if the implementation returns fields not in the spec or omits required fields."
Prompt 2: Convert Specs to BDD
"I have this Markdown specification with acceptance criteria: [paste spec]. Convert each acceptance criterion into a Gherkin BDD scenario. Ensure every scenario is specific, testable, and covers exactly one behavior."
Prompt 3: Build a Validation Pipeline
"I need a GitHub Actions workflow that validates my implementation against three specification types: OpenAPI contract, BDD scenarios, and database schema. Show me the complete workflow file with all necessary steps."
Practice Exercises
Exercise 1: Anchor an Existing API
Take an existing API endpoint in your project. Write the OpenAPI specification for its current behavior (reverse-engineer it). Then set up contract validation. Run it. Does the implementation match the spec you wrote?
Expected outcome: You'll likely discover undocumented response fields, inconsistent error formats, or missing validation — gaps that were invisible without the spec anchor.
Exercise 2: BDD for a Critical Flow
Choose the most important user flow in your application (login, checkout, onboarding). Write BDD scenarios for every path — happy path, error paths, edge cases. Implement them as automated tests.
Expected outcome: 10-20 scenarios covering behaviors you may not have been testing, with at least 2-3 failures revealing existing bugs.
Exercise 3: Schema Constraint Audit
Review your database schema for business rules that should be enforced at the database level but are currently only enforced in application code. Document each rule and create the appropriate constraint, trigger, or check.
Expected outcome: 3-5 business rules moved from fragile application-level enforcement to robust database-level enforcement.
Key Takeaways
-
Spec-anchored development makes specifications executable — they can be validated automatically, eliminating the drift problem.
-
Three pillars anchor specifications to code: API contracts (OpenAPI), behavioral specifications (BDD), and schema definitions.
-
Automated validation in CI pipelines turns specification violations into build failures, forcing every change to be either spec-compliant or spec-updating.
-
The forced decision between "fix code" and "update spec" eliminates the gray zone where code drifts silently.
-
Teams should start spec-first and evolve to spec-anchored as projects mature and drift risks increase.
Chapter Quiz
-
What problem does spec-anchored development solve that spec-first does not?
-
Name the three pillars of spec-anchored development and give one example of each.
-
Why is BDD considered both a specification and a test?
-
When a spec-anchored build fails, what are the two options available to the developer?
-
What is the difference between a database constraint and an application-level validation?
-
When should a team transition from spec-first to spec-anchored development?
-
What are the tradeoffs of requiring machine-readable specification formats vs. natural language?
-
How does an OpenAPI specification prevent API drift?