Spec Driven Development Part 2: The Anatomy of a Good Spec
14 min read

Spec Driven Development Part 2: The Anatomy of a Good Spec

In Part 1, we discussed why specs become the source of truth in Spec-Driven Development, and that an effective spec contains four elements: intent, constraints, acceptance criteria, and non-goals. But knowing these four elements isn’t enough — many teams have already written “specs” that formally have all four sections, yet still produce code that deviates from what was intended. The problem is in the quality of each element’s content, not just its existence. This article discusses how to concretely write each element, practical notation for making acceptance criteria truly testable, and how to recognize specs that look neat but are actually weak.

Why a Bad Spec Is More Dangerous Than No Spec at All

This sounds contradictory, but it makes sense when thought through from the team psychology side. When there’s no spec at all, everyone — including the agent — knows they’re improvising. There’s extra vigilance during review, because all parties realize ambiguity is real.

A bad spec removes this vigilance. A document that looks structured, with “Requirements” and “Acceptance Criteria” headings, gives a false sense of security. Reviewers assume that because a spec exists, the code must match the spec — even though the spec itself is full of ambiguities letting the agent (or developer) interpret freely as they wish.

The most common characteristics of weak specs:

  • Subjective language without measures — words like “fast”, “secure”, “user-friendly” without concrete definitions of what fast, secure, or user-friendly means in this context
  • Acceptance criteria that can’t be tested — criteria written as general statements, not conditions checkable as pass/fail
  • Constraints assumed, not stated — the spec author assumes “of course it’s obvious”, even though the agent has no access to that implicit assumption
  • Scope that only explains what’s built, not what isn’t — opening room for the agent to add unrequested features
A spec full of headings that looks formal isn’t a quality guarantee. The real measure is: can two different people (or two agents) read this spec and produce functionally identical code? If not, the spec is still ambiguous.

Before getting into each element’s details, here’s how the four relate to each other in one complete spec:

flowchart TD
    A[Intent: why this is built] --> B[Constraint: limits that must not be violated]
    B --> C[Acceptance Criteria: when it's considered done]
    C --> D[Non-Goals: what's explicitly out of scope]
    D --> E[Spec Ready for Review]
    A -.becomes the decision context when ambiguous.-> C

Intent provides the context used to make decisions when acceptance criteria don’t cover every case. Constraints and non-goals both function as boundaries, but from opposite directions — constraints limit how the implementation may be done, non-goals limit what is included in scope.

Element 1: Intent

Intent answers the question “why does this feature need to be built” — not just “what should be built”. This is the element most often skipped because it feels like small talk, even though intent is the context helping the agent (and human reviewers) make the right decisions when the spec doesn’t cover every possible case.

Weak intent usually just repeats the feature title:

Intent: Create a password reset feature.

This isn’t intent, it’s a label. There’s no information about what problem is being solved, who’s affected, or why now.

Strong intent explains the problem, impact, and urgency:

Intent: Currently users who forget their passwords must contact support
manually, which takes an average of 4 hours of response time and accounts
for 23% of total monthly support tickets. A self-service password reset
feature will eliminate the dependency on support for this case, while
improving security because the verification process is standardized
(currently manual verification by support is vulnerable to social
engineering).

Notice the difference: the second version gives the agent an understanding of what matters and why. When there’s later an implementation decision not explicitly mentioned in the spec — for example, whether the password reset endpoint needs rate limiting — an agent understanding the intent (reducing social engineering risk) is more likely to make a decision aligned with the original goal, compared to an agent who only knows “create a password reset feature”.

Good intent usually answers three things at once: what problem exists now, who is affected by that problem, and why this solution is now a priority.

Element 2: Constraints

Constraints are limits the implementation must obey — whatever the approach, these limits must not be violated. Constraints differ from acceptance criteria: acceptance criteria define “when it’s considered done”, while constraints define “limits that must not be crossed throughout the process”, including things that might not be directly visible from the final result.

Constraints usually fall into several categories:

CategoryExample
TechnicalMust be compatible with the existing database, must not add new dependencies without approval
SecurityPassword reset tokens must expire within 15 minutes, must never log passwords in any form
PerformanceThe endpoint must respond under 200ms at p95, queries must not do full table scans
CompatibilityThe API must be backward compatible with the mobile app version still used by 5% of active users
Regulatory/compliancePersonal data must be permanently deletable on user request (right to erasure)

The often missed part: constraints must be stated explicitly even when “you think it’s obvious”. Agents don’t have years of experience working in the same codebase like senior developers on the team. Assumptions that seem “of course it’s like that” to a senior developer need to be written down, because for an agent they’re not certain at all.

ANTI-PATTERN (constraint assumed):

"Implement the password reset endpoint as usual."

CORRECT (explicit constraints):
Constraint:
- The reset token must be a UUID v4, stored hashed (not plaintext)
  in the database, and expire 15 minutes after creation
- The endpoint must be rate limited to 3 requests per email per hour
- Must not reveal whether an email is registered or not
  (preventing email enumeration)
- Must use the existing email service (SendGrid), must not add a new provider
Security constraints are the most dangerous category if assumed rather than stated. An agent can produce code passing all functional acceptance criteria but opening security holes, because no explicit constraint forbids it. Always state security constraints explicitly, don’t assume it’s “common sense”.

Element 3: Acceptance Criteria

Acceptance criteria define when an implementation is considered done and correct — and this must be objectively verifiable, ideally automatically. This is the element most directly connected to testing, and will be discussed deeper in Part 5 of this series.

The most common way to fail at writing acceptance criteria is writing them as qualitative statements:

ANTI-PATTERN:
- The system must handle errors well
- Password reset must be secure
- Performance must be fast

None of these three lines can be checked pass/fail by anyone, including the agent. “Handle well” according to whom? “Secure” against what threat? “Fast” in how many milliseconds?

One fairly popular notation for writing testable acceptance criteria is EARS — Easy Approach to Requirements Syntax. EARS provides several standard sentence patterns forcing requirements to be written with clear conditions and results:

EARS PatternFormatWhen Used
Ubiquitous“The system shall [behavior]”Requirements that always apply, without conditions
Event-driven“When [trigger], the system shall [behavior]”Responses to specific events
State-driven“While [state], the system shall [behavior]”Behavior depending on the system’s current state
Unwanted behavior“If [undesired condition], then the system shall [behavior]”Error handling or invalid cases
Optional“Where [optional feature is available], the system shall [behavior]”Behavior only applying when a certain feature is active

With this pattern, the three bad acceptance criteria lines above can be rewritten as:

CORRECT (EARS format):
- When a user submits a registered email to the password reset endpoint,
  the system shall send an email containing a reset link within a maximum of 5 seconds
- When a user submits an unregistered email, the system shall still
  respond with the same success message (preventing email enumeration)
- If a reset token has expired, then the system shall reject the request
  with the message "link is no longer valid" and HTTP code 410
- If a reset token has already been used, then the system shall reject the
  second request with the same message and the token must be immediately deleted from the database
- While the reset process is ongoing, the system shall record an audit log containing
  a timestamp and IP address (without recording passwords or tokens in plaintext)

Every line in this version can be directly derived into one or more test cases. There’s no room for interpretation about when a criterion is met or not.

A quick trick to check whether acceptance criteria are good enough: try imagining writing an automated test from that criterion without asking anyone anything. If you still need clarification to know what to assert, the criterion isn’t specific enough.

Element 4: Non-Goals

Non-goals explicitly state what is not included in the scope of this work. This element is often considered unnecessary — “it’s obvious from the feature title” — when in fact this is where one of the most common sources of AI agent over-engineering lies.

An agent given a spec without non-goals tends to interpret broadly. Asked to create “password reset”, the agent might also add the “change password without reset” feature, or build a more complex email notification system than needed, because it feels “related” and “helpful”. Without explicit boundaries, this scope expansion happens silently and is only discovered at review — or worse, not discovered at all until it becomes technical debt.

Non-Goals:
- Does NOT include changing passwords from the profile page (already exists,
  separate endpoint)
- Does NOT include push or SMS notifications — email only
- Does NOT include an audit dashboard for viewing password reset history
  (will be a separate feature)
- Does NOT change the existing users table schema — only adds a new
  password_reset_tokens table

Non-goals are also useful as a communication tool with non-technical stakeholders. When someone asks “why is feature X missing”, the answer is already written in the spec from the start — this isn’t an oversight, but a conscious decision.

Without non-goals, scope tends to silently expand during execution, not from the start:

flowchart LR
    A[Requested Scope] -->|without non-goals| B[Agent Interprets Broadly]
    B --> C['Related' Extra Features]
    C --> D[Scope Grows Unnoticed]
    A -->|with explicit non-goals| E[Agent Stays Within Bounds]
    E --> F[Scope as Planned]

A Complete Spec Example (Case Study)

Here’s a generic markdown spec example combining all four elements, for the password reset feature already used as an example above. This format isn’t tied to any programming language or specific tool — it can be adapted to whatever spec kit format the team uses.

# Spec: Self-Service Password Reset

## Intent
Currently users who forget their passwords must contact support manually,
which takes an average of 4 hours of response time and accounts for 23%
of total monthly support tickets. A self-service password reset feature
will eliminate the dependency on support for this case, while improving
security because the verification process is standardized.

## Constraints
- The reset token must be a UUID v4, stored hashed (not plaintext) in the
  database, expiring 15 minutes after creation
- The endpoint must be rate limited to 3 requests per email per hour
- Must not leak whether an email is registered or not
- Must use the existing email service (SendGrid), must not add a new provider
- Must not change the existing users table schema

## Acceptance Criteria
- When a user submits a registered email to the password reset endpoint,
  the system shall send an email containing a reset link within a maximum of 5 seconds
- When a user submits an unregistered email, the system shall still
  respond with the same success message
- If a reset token has expired, the system shall reject the request with
  HTTP 410 and the message "link is no longer valid"
- If a reset token has already been used, the system shall reject the second
  request and immediately delete the token from the database
- When a user submits a new password with a valid token, the system shall
  store the new password (hashed) and delete that token
- If the new password doesn't meet the password policy (minimum 8 characters,
  letter and number combination), the system shall reject it with a clear
  validation message
- While the reset process is ongoing, the system shall record an audit log
  containing a timestamp and IP address, without recording passwords or tokens
  in plaintext

## Non-Goals
- Does NOT include changing passwords from the profile page
- Does NOT include push or SMS notifications — email only
- Does NOT include an audit dashboard for viewing password reset history
- Does NOT change the existing users table schema

A spec this long looks time-consuming to write, but compare it with the time usually lost to the repeated “generate — review — revise — regenerate” cycle when the spec is vague. This upfront investment is almost always cheaper.

Bad Spec vs Good Spec — A Direct Comparison

To see the impact directly, here’s the same requirement written with two different quality levels.

Scenario: a product search feature in e-commerce

BAD SPEC:
Create a fast and relevant product search feature. Search results must be
accurate and the user experience good. Make sure the performance is optimal
even though there's a lot of product data.

This spec doesn’t tell the agent: what “fast” means in numbers, how relevance is measured, which fields are searched, how to handle typos, or what happens if the results are empty. The agent will fill all these gaps with its own assumptions — and those assumptions are likely different from what the spec author imagined.

GOOD SPEC:

Intent: Users currently struggle to find products because search only
matches product titles with exact match, causing 31% of searches to return
no results even though the searched product exists in the catalog.

Constraints:
- Must use the already-deployed Elasticsearch, must not add a new search engine
- The index must be updated within a maximum of 5 minutes after a product changes
- Must not expose internal fields (cost price, supplier info) in the API response

Acceptance Criteria:
- When a user searches with a keyword present in a product's title or
  description, the system shall return that product within the top 10 results
- When a user searches with a light typo (maximum edit distance of 2),
  the system shall still display relevant results
- The system shall respond within a maximum of 300ms at p95 for catalogs
  of up to 1 million products
- If no results match, the system shall display the 5 best-selling products
  in the same category as recommendations
- Search results shall be sorted by relevance score, with out-of-stock
  products displayed last

Non-Goals:
- Does NOT include image-based search (visual search)
- Does NOT include result personalization based on user history
- Does NOT change the Elasticsearch index structure used by other features

The second version gives the agent clear boundaries to work within: what technology is used, how typo-tolerant it should be, performance targets in numbers, and what’s explicitly out of scope. The implementation result from this spec is far more predictable than the first version.

Common Anti-Patterns in Writing Specs

Besides specs that are too vague, there’s also the opposite direction that’s equally problematic.

Over-specification — a spec written down to pseudocode level, dictating the code structure line by line. This removes the main advantage of working with an agent: its ability to find good implementation approaches. A spec should define what must be correct, not how to do it line by line, unless there’s indeed a technical constraint requiring a specific approach.

ANTI-PATTERN (over-specified):
Create a function named validateEmail that takes a string parameter,
then inside the function create a regex variable, then use regex.test(),
then if false return the object {valid: false, error: "invalid"}...

CORRECT:
Acceptance Criteria:
- If the email format is invalid (no @ or domain), the system shall
  reject the input with an error message mentioning which field is wrong

Under-specification — the opposite: a spec too abstract that all important decisions are handed to the agent. This usually happens when the spec author is in a hurry and assumes the details “will become clear by themselves during implementation”. The clearest sign: acceptance criteria that two different people can interpret in more than one way.

Contradictory specs — especially occurs in specs written incrementally or the result of repeated edits. For example a constraint says “must not add new dependencies” but acceptance criteria require a feature that practically needs a certain library. An agent finding this contradiction usually picks one randomly, or worse, tries to “game” both with a strange solution. Reviewing the spec before execution (the second phase of the loop discussed in Part 1) exists precisely to catch such contradictions before code is written.

There’s no definite formula for “how detailed” a spec should be. The practical guideline: detailed enough to remove the important ambiguities, but loose enough to let the agent choose a good implementation approach within the existing constraints.

Summary

  • A formal-looking spec isn’t automatically high quality — the real measure is whether two different parties can produce functionally identical code from the same spec
  • Intent must explain the problem, who’s affected, and why now — not just repeat the feature title
  • Constraints must be stated explicitly, especially for the security category, because agents don’t have access to the implicit assumptions senior developers hold
  • Acceptance criteria must be objectively verifiable; EARS notation (ubiquitous, event-driven, state-driven, unwanted behavior, optional) helps write criteria directly derivable into test cases
  • Non-goals prevent scope creep and over-engineering from agents that tend to interpret requirements broadly
  • Avoid the two opposing anti-patterns: over-specification (dictating implementation down to pseudocode level) and under-specification (too abstract until all important decisions are handed to the agent)
  • Contradictory specs — for example constraints and acceptance criteria conflicting with each other — must be caught at the review phase, before execution begins

Portfolio