Spec-Driven Development sdd-2 30 min

Writing Specs That Agents Can Act On

Learning Objectives

  • write a complete spec for each of the four spec types
  • apply the four-element framework to a new task from scratch
  • construct a spec using the provided templates
  • adapt an existing spec template to fit a different task context

Core Concepts

The four spec types form a chain from intent to verified output. Writing the right type for the task means the agent has the right structure of information for its role in that chain.

The Four Spec Types in Context

Each spec type answers a different question and targets a different stage of the work.

Product Spec answers: what needs to be built and why does it matter to users? A Product Spec for a rescheduling feature on the scheduling platform defines the user problem (clients need to change their booking without cancelling), the scope (reschedule via email link, single rebooking, no payment changes), and the high-level criteria that confirm the feature works (new slot confirmed, original slot released, both parties notified).

Functional Spec answers: how should the system behave, step by step and rule by rule? A Functional Spec for the client booking page defines the user flow from landing to confirmation, the system behaviour at each step (slot holds for 60 seconds during checkout), and the business rules the system must enforce (no double-booking, no past-date selection, confirmation email within 30 seconds).

Technical Spec answers: how will this be implemented, and what are the architectural decisions? A Technical Spec for the booking reservation endpoint defines the data model, the API surface, the concurrency approach (row-level lock on slot reservation), and the technology constraints (PostgreSQL, parameterised queries, service layer pattern).

Test Spec answers: what scenarios confirm the system behaves as specified? A Test Spec for the booking confirmation flow defines UI scenarios (client books last available slot, second client sees slot gone), API cases (POST /api/bookings returns 409 on conflict), integration checks (confirmation email delivered within 10 seconds), and validation rules (attendee count never exceeds slot capacity).

The Four-Element Framework

Every effective spec, regardless of type, contains four elements:

  1. Context: what situation or system does this spec operate in?
  2. Objective: what exactly needs to be produced or done?
  3. Constraints: what boundaries, rules, or non-negotiable requirements apply?
  4. Output format: what does the deliverable look like, and how will it be validated?

Omitting any element forces the agent to make assumptions. Those assumptions are usually wrong in ways that are expensive to fix.

Key Points

  • Four spec types: Product, Functional, Technical, Test
  • Each type answers a distinct question at a different stage of the chain: intent, behaviour, implementation, verification
  • Every spec must contain context, objective, constraints, and output format
  • The right spec type depends on the nature of the work, not personal preference
  • A spec written for a human reader is not automatically a spec an agent can act on

Tools, Prompts, or Templates

Product Spec Template

A product spec without explicit scope produces an agent that interprets user need as an invitation to build the most ambitious version of the feature. The template below forces you to state what is in scope, what is out, and what a successful outcome looks like for users, before any design or implementation begins.

Worked Example: Slot Rescheduling

PRODUCT SPEC
============

Feature Name:    Slot Rescheduling
Status:          Ready for scoping

User Need:
  Clients who have confirmed bookings sometimes need to change the time
  without cancelling and rebooking. Currently they must cancel, return
  to the booking page, and start over. This creates unnecessary drop-off
  and generates cancellation notifications that feel disruptive to hosts.

Features and Scope:
  In scope:
    - Client requests a reschedule via a link in their confirmation email
    - Client selects a new available slot from the host's current availability
    - A rescheduled confirmation email is sent to both host and client
    - The original slot is released and the new slot is confirmed atomically

  Out of scope (v1):
    - Host-initiated rescheduling
    - Rescheduling with a payment change
    - Rescheduling within 2 hours of the original slot start time
    - Waitlist promotion when a slot is released by a reschedule

High-Level Acceptance Criteria:
  - A client can reschedule using the link in their original confirmation email
  - The new slot must be within the host's current published availability
  - Both host and client receive a rescheduled confirmation email
  - The original slot becomes available to other clients immediately
  - A client cannot reschedule the same booking more than once within 24 hours

Generic Template

PRODUCT SPEC
============

Feature Name:    [Human-readable feature name]
Status:          [Discovery | Ready for scoping | Ready for design]

User Need:
  [2–4 sentences: what problem does this feature solve, for whom, and
  what does the current experience look like without it? Ground it in
  a real user situation.]

Features and Scope:
  In scope:
    - [Specific capability included in this version]
    - [Specific capability included in this version]

  Out of scope:
    - [Capability explicitly deferred to a future version]
    - [Related feature handled elsewhere or not yet decided]

High-Level Acceptance Criteria:
  - [Observable, user-facing outcome that confirms the feature works]
  - [Observable, user-facing outcome that confirms the feature works]
  - [Boundary condition: what the feature must not do]

Key fields:

  • User Need: answers the question "why are we building this?" before any agent touches the design
  • Out of scope: the most leverage you have over agent scope creep; state it explicitly before any design begins
  • High-Level Acceptance Criteria: user-facing, not system-facing; these become the top-level review criteria for the whole feature

Functional Spec Template

A functional spec without explicit business rules produces code that handles the happy path and ignores everything that breaks it. The template below forces you to state user flows, system behaviour at each step, and the rules the system must enforce, before implementation begins. This is the layer where most feature failures originate: not in the code, but in the gap between what was built and what the system was supposed to do.

Worked Example: Client Booking Flow

FUNCTIONAL SPEC
===============

Feature Name:    Client Booking Flow
Route:           /book/{host-slug}
Status:          Ready for implementation

User Flow:
  1. Client navigates to /book/{host-slug}
  2. Page loads host profile (name, avatar, short bio) and a date picker
  3. Client selects a date; available 30-minute slots load for that date
  4. Client selects a slot; a 60-second hold is placed on that slot to
     prevent another client from booking it during checkout
  5. Client enters their name and email address and clicks "Confirm Booking"
  6. System validates the slot is still held and not booked by another client
  7. System creates a booking record and releases the hold
  8. System dispatches confirmation emails to host and client
  9. Client is redirected to /confirmation/{booking-id}

System Behaviour:
  - Slot availability is calculated in real time from the host's availability
    windows, excluding confirmed bookings and synced calendar events
  - A slot that is on hold (60-second checkout window) appears as unavailable
    to other clients during that period
  - If the hold expires before the client confirms, the slot is released
    automatically and the client sees an inline error prompting them to
    re-select
  - If the slot is booked between hold placement and confirmation (race
    condition), the client receives an inline error and the slot list refreshes
    automatically

Business Rules:
  - A client cannot book a slot that starts in the past
  - A slot that already has a confirmed booking cannot be booked again
  - Confirmation email must dispatch within 30 seconds of booking creation
  - If no slots are available for the selected date, display:
    "No available slots on this date. Try another day."
  - Page must be accessible without authentication; hosts are identified
    by their public slug

Edge Cases:
  - Host slug not found: render a 404 page with a return link
  - All slots on the selected date are held or booked: show the
    "no available slots" message, not an empty list
  - Email dispatch fails: log the failure, mark notification_sent = false,
    retry via background job; do not block the booking confirmation flow

Generic Template

FUNCTIONAL SPEC
===============

Feature Name:    [Human-readable name]
Route:           [URL pattern, if applicable]
Status:          [Draft | Ready for implementation | Under review]

User Flow:
  1. [Actor + action at step 1]
  2. [Actor + action at step 2]
  3. [Continue until the flow reaches its successful end state]

System Behaviour:
  - [What the system does at a specific step or in response to a condition]
  - [What the system does when a constraint is triggered]
  - [What the system does when the happy path is interrupted]

Business Rules:
  - [Rule the system must enforce, stated as a constraint on system action]
  - [Rule the system must enforce, stated as a constraint on system action]
  - [User-facing message that must appear in a specific condition]

Edge Cases:
  - [Error condition]: [exact system behaviour or user-facing message]
  - [Error condition]: [exact system behaviour or user-facing message]

Key fields:

  • User Flow: numbered steps prevent the agent from inventing a different interaction model
  • Business Rules: stated as constraints on the system, not descriptions of the UI; the agent enforces these in code
  • Edge Cases: the most commonly skipped section; an agent will not generate robust error handling without them

Technical Spec Template

A technical spec without explicit architecture decisions produces code that works in isolation but conflicts with the rest of the system. The template below forces you to state the data model, the API surface, the concurrency approach, and the technology constraints before a coding agent writes a single line. Without it, the agent makes architecture decisions on its own.

Worked Example: Booking Reservation Endpoint

TECHNICAL SPEC
==============

Component Name:  Booking Reservation Endpoint
Status:          Ready for implementation

Architecture:
  The booking reservation handler lives in the service layer, not the
  route handler. The route handler validates the request and delegates
  to BookingService.reserve(slotId, clientData). The service layer
  owns all business logic, database operations, and job enqueueing.

Data Model:
  bookings
    id              UUID          primary key
    host_id         UUID          foreign key → hosts.id
    client_name     VARCHAR(200)  not null
    client_email    VARCHAR(200)  not null
    start_time      TIMESTAMPTZ   not null
    end_time        TIMESTAMPTZ   not null
    status          ENUM          ('pending', 'confirmed', 'cancelled')
    notification_sent  BOOLEAN    default false
    created_at      TIMESTAMPTZ   default now()

  slot_holds
    id              UUID          primary key
    slot_start      TIMESTAMPTZ   not null
    host_id         UUID          foreign key → hosts.id
    held_until      TIMESTAMPTZ   not null (now + 60 seconds)
    session_token   UUID          identifies the checkout session holding this slot

API Endpoints:
  POST /api/bookings
    Request:  { host_slug, slot_start, client_name, client_email }
    Response: 201 { booking_id, confirmation_url }
              409 { error: "slot_unavailable", message: "..." }
              422 { error: "validation_error", fields: [...] }

  POST /api/slot-holds
    Request:  { host_slug, slot_start, session_token }
    Response: 201 { held_until }
              409 { error: "slot_unavailable" }

Concurrency Approach:
  Slot reservation uses a row-level lock on the slot_holds record:
    BEGIN;
    SELECT id FROM slot_holds
      WHERE host_id = $1 AND slot_start = $2
      FOR UPDATE NOWAIT;
    -- if row exists and held_until > now(): return 409
    -- if row does not exist: insert hold, proceed with booking
    COMMIT;
  This prevents two concurrent requests from reserving the same slot.
  NOWAIT is used so the second request fails immediately rather than
  waiting for the lock.

System Integrations:
  - Notification service: enqueue booking_id to the confirmation email
    queue on successful booking; do not call the email service inline
  - Calendar sync: enqueue booking_id to the calendar sync queue after
    booking is committed; do not await the sync result

Tech Stack Constraints:
  - Backend: Node.js 20, Express 4, node-postgres (pg)
  - Database: PostgreSQL 15; parameterised queries only (no string
    interpolation in any SQL)
  - ORM: none; raw SQL via node-postgres query builder
  - Timezone: all times stored as TIMESTAMPTZ (UTC); display conversion
    happens on the frontend

Generic Template

TECHNICAL SPEC
==============

Component Name:  [Human-readable name]
Status:          [Draft | Ready for implementation | Under review]

Architecture:
  [2–4 sentences: how does this component fit into the existing system?
  Which layer does it live in? What does it delegate to and what does
  it receive from?]

Data Model:
  [table_name]
    [column_name]   [type]   [constraints and notes]
    [column_name]   [type]   [constraints and notes]

API Endpoints:
  [METHOD] [/path]
    Request:  [payload shape]
    Response: [status code + payload for each outcome]

Concurrency Approach:
  [If this component handles concurrent requests or writes: describe the
  locking or isolation strategy. State it explicitly; do not leave it
  for the agent to decide.]

System Integrations:
  - [Service or system this component calls, and how (sync/async/queue)]
  - [External API or queue, and what gets enqueued or called]

Tech Stack Constraints:
  - [Language, framework, and version]
  - [Database technology and query pattern]
  - [Performance or security constraint]
  - [Convention that must be followed: naming, file location, pattern]

Key fields:

  • Data Model: column names and types must match the actual schema; mismatches here produce code that fails at the database boundary
  • Concurrency Approach: the most commonly skipped section; agents will not implement locking without explicit instruction
  • System Integrations: state sync vs. async explicitly; agents default to synchronous calls when async is required

Test Spec Template

A test spec derived from the functional spec produces verification that confirms the system behaves as designed. A test spec written from the implementation produces tests that pass when the code is wrong in the same way as the tests. The template below forces you to derive test cases from functional requirements, cover UI, API, and integration paths, and state expected outcomes before any test code is written.

Worked Example: Booking Confirmation Tests

TEST SPEC
=========

Feature:         Client Booking Flow (see functional spec)
Status:          Ready for test generation

Test Cases:
  UI Scenarios:
    TC-01: Single client books available slot
      Setup:    Host has one available slot on Wednesday at 14:00
      Action:   Client navigates to booking page, selects Wednesday 14:00,
                enters name and email, clicks "Confirm Booking"
      Expected: Booking record created with status "confirmed"; client
                redirected to /confirmation/{booking-id}; confirmation
                email dispatched to both parties

    TC-02: Two clients race for the last slot
      Setup:    Host has one available slot at 14:00; two clients load
                the booking page simultaneously
      Action:   Both clients select 14:00 and submit within the same second
      Expected: One client receives confirmation and is redirected;
                second client sees inline error "This slot was just booked.
                Please select another time." and the slot list refreshes
                with 14:00 no longer available

    TC-03: Client selects a date with no available slots
      Setup:    All of the host's Wednesday slots are confirmed bookings
      Action:   Client selects Wednesday on the date picker
      Expected: "No available slots on this date. Try another day."
                message is displayed; no empty slot grid is rendered

  API Scenarios:
    TC-04: POST /api/bookings returns 201 on valid request
      Input:    { host_slug: "dr-chen", slot_start: "2026-06-10T14:00:00Z",
                  client_name: "Alice Lee", client_email: "[email protected]" }
      Expected: 201 { booking_id: "<uuid>", confirmation_url: "/confirmation/<uuid>" }
                booking record in database with status "confirmed"

    TC-05: POST /api/bookings returns 409 on conflict
      Setup:    Slot at 14:00 has a confirmed booking
      Input:    Same slot_start as confirmed booking
      Expected: 409 { error: "slot_unavailable",
                       message: "This slot is no longer available." }
                No new booking record created

    TC-06: POST /api/bookings returns 422 on invalid email
      Input:    { client_email: "not-an-email" }
      Expected: 422 { error: "validation_error",
                       fields: [{ field: "client_email",
                                  message: "Invalid email address" }] }

  Integration Scenarios:
    TC-07: Confirmation email delivered within 10 seconds
      Action:   Complete a booking via POST /api/bookings
      Expected: Email to client and host address appears in test inbox
                within 10 seconds of the 201 response

    TC-08: Email failure does not block booking confirmation
      Setup:    Email service returns 503 on first attempt
      Action:   Complete a booking
      Expected: Booking record has status "confirmed"; notification_sent = false;
                retry job enqueued; 201 response returned to client without delay

Validation Rules:
  - booking.status must be "confirmed" immediately after POST /api/bookings 201
  - notification_sent must not be set to true until email dispatch is confirmed
    by the email service (not just enqueued)
  - slot_holds record for the booked slot must be deleted after booking creation
  - No booking record should exist in the database after a 409 response

Generic Template

TEST SPEC
=========

Feature:         [Feature name and reference to functional spec]
Status:          [Draft | Ready for test generation | Active]

Test Cases:
  UI Scenarios:
    [TC-ID]: [Scenario name]
      Setup:    [Initial state of the system before the action]
      Action:   [What the user does, step by step]
      Expected: [Exact observable outcome: what the user sees, what
                 changes in the system]

  API Scenarios:
    [TC-ID]: [Scenario name]
      Input:    [Request payload or parameters]
      Expected: [HTTP status code + response body + database state]

  Integration Scenarios:
    [TC-ID]: [Scenario name]
      Action:   [What triggers the integration]
      Expected: [Observable outcome across system boundaries: email,
                 calendar event, queue entry, etc.]

Validation Rules:
  - [Database invariant: a field or record state that must hold after
    every successful or failed operation]
  - [System invariant: a condition that must never be violated under
    any test scenario]

Key fields:

  • Setup: specifies the exact system state before each test; agents without setup context generate tests with undeclared dependencies
  • Expected: one observable outcome per test case, stated precisely enough that pass/fail is unambiguous
  • Validation Rules: database and system invariants that apply across all test cases, not just the happy path

Actionable Takeaways

  • Pick one task you are currently working on and identify which spec type it requires before writing a single line of code or a prompt.
  • Write the Out of Scope section of your product spec first. It forces you to make exclusion decisions early, when they are cheap.
  • When writing a functional spec, list business rules and edge cases before the user flow. Rules that emerge from the flow are easier to catch before you write the steps than after.
  • When writing a technical spec, state the concurrency approach explicitly. If you are uncertain what it should be, write "TBD: locking strategy" -- that gap will surface the ambiguity rather than hiding it.
  • Derive test cases from the functional spec, not the code. Each test case should reference a business rule or user flow step; tests that reference implementation details drift when the code changes.

Practical Examples

Example 1: Product Manager Scoping a Feature (Product Spec)

A product manager is asked to scope the team scheduling feature: the ability for an organisation to share a unified booking page across multiple hosts. She writes a product spec before any design begins.

The user need section explains the problem (clients booking with a team do not know which host is available; they currently contact the team by email to arrange a time). The scope section defines what is in v1 (a team booking page where the system selects the next available host) and what is not (client preference for a specific host, round-robin balancing rules, custom team branding). The acceptance criteria state three observable outcomes: a client can book without knowing individual host names; the system assigns the booking to the first available host; both the assigned host and the client receive a confirmation.

The agent working from this spec scopes the feature correctly. The team booking page it produces handles the assignment flow without building host preference selection, which was explicitly out of scope.

Example 2: Engineering Lead Speccing System Behaviour (Functional Spec)

An engineering lead hands off the cancellation flow to a developer using a coding agent. Before any code is written, she writes a functional spec.

The user flow covers the full interaction: client navigates to their booking, selects cancel, confirms, and is redirected to a cancellation confirmation page. The system behaviour section specifies the sequence: slot is released immediately on cancellation, not on email delivery. The business rules include the late-cancellation rule: a cancellation within 24 hours of the booking start time adds a flag to the client's account, and the host can waive the flag manually. Edge cases cover what happens if the cancellation link has expired, if the booking is already cancelled, and if the email service is unavailable.

The agent generates the cancellation handler with the correct sequencing -- slot release before notification -- and implements the late-cancellation flag as an explicit check rather than an assumption.

Example 3: Senior Engineer Defining the Architecture (Technical Spec)

A senior engineer is building the Google Calendar sync adapter. Before directing the coding agent, she writes a technical spec.

The architecture section defines where the adapter lives (an isolated CalendarSyncAdapter class, called via a job queue event after booking confirmation, not inline in the booking handler). The data model section defines the CalendarSync table: internal booking ID mapped to external Google Calendar event ID for idempotency. The API section specifies the Google Calendar Events API v3 endpoint, the OAuth 2.0 flow, and how token refresh is handled. The concurrency approach covers what happens on retry: because the booking ID is mapped to a calendar event ID, a second call for the same booking checks the mapping table before creating a new event. The tech constraints include that no new dependencies can be introduced beyond the existing Google API client library.

The agent generates the adapter with correct idempotency handling because the technical spec stated the mapping approach explicitly. Without the spec, the agent would have generated a naive implementation that creates duplicate calendar events on retry.

Example 4: QA Lead Writing Test Scenarios (Test Spec)

A QA lead writes a test spec for the group booking feature after the functional spec is approved, before any implementation begins.

The UI scenarios section includes the concurrent booking case: two clients attempt to book the last spot simultaneously, one receives a confirmation, the other sees a real-time "session full" message. The API scenarios section specifies the concurrent load test: POST /group-sessions/{id}/bookings with 10 simultaneous requests should return exactly one 200 and nine 409 responses. The integration scenario verifies the host's calendar event appears within 5 seconds of the first confirmed booking. The validation rules section adds two database invariants: attendee count must never exceed max_attendees, verified after every API test run; and no booking record should exist for a 409 response.

The test-generation agent produces test code that maps to functional requirements rather than implementation internals. When the implementation changes, the tests are retargeted by updating the functional spec, not the test code.


Implementation Workflow

  1. Review your notes from Lesson 3: The Four Spec Types. Identify one task you have not yet applied in practice.

  2. Choose the spec type that matches the stage of work: use a Product Spec if you are defining what to build, a Functional Spec if you are describing system behaviour, a Technical Spec if you are specifying the implementation, or a Test Spec if you are defining verification scenarios.

  3. Copy the Generic Template for that spec type from this lesson into a new file in your project.

  4. Fill in every section. Do not skip Out of Scope (Product Spec), Edge Cases (Functional Spec), Concurrency Approach (Technical Spec), or Validation Rules (Test Spec). These are the sections most commonly omitted and most commonly the cause of agent output that misses the real requirement.

  5. Before prompting the agent, read back what you have written. For each section, ask: if the agent reads only this section and ignores the rest, does it have enough to do the right thing? If not, add the missing information.

  6. Hand the spec to the agent as its first message, prefixed with: "Act on the following spec. If any field is ambiguous, ask one clarifying question before proceeding." If the agent asks about something you already specified, your spec has a gap: fill it before running the next session.

  7. Evaluate the output against the spec. For a product spec, check whether the scope boundary was respected. For a functional spec, check whether the business rules are enforced. For a technical spec, check whether the architecture matches. For a test spec, check whether each test case maps back to a stated requirement. Keep this output: you will compare it against a second iteration in Lesson 5: The Spec → Generate → Review Loop.