Spec-Driven Development: Why Planning Matters More Than Ever with AI
Use evolving specifications to guide AI implementation, assess architecture and maintenance costs, and connect acceptance criteria to verification.
AI coding agents can make a large change before a reviewer notices that the requirement was misunderstood. A short specification helps move those disagreements earlier, when changing the decision costs less than replacing the implementation.
Agents can also inspect code and ask questions. The problem is not that they are incapable of clarification; it is that a workflow may reward immediate implementation without making uncertainty visible.
Spec-driven development (SDD) gives requirements and acceptance criteria an explicit role in implementation and verification. The label covers different workflows rather than one universal method. GitHub Spec Kit is one concrete toolkit organized around specifications, plans, and implementation tasks.
This builds on Harness Engineering: execution tools need a clear target.
Specify behavior before choosing an implementation
A specification should capture the decisions needed to assess a change. It does not need to predict every line of code.
For a small bug, a reproduction and expected result may be sufficient. A change to a shared API deserves more detail: compatibility, error behavior, concurrent requests, and rollout expectations.
Separate three artifacts when the task is substantial:
- Specification: The observable behavior, constraints, and exclusions.
- Plan: The proposed implementation and verification steps.
- Evidence: The checks and observations showing what the implementation actually does.
A good plan cannot repair an incorrect requirement. Passing checks cannot establish behavior they never exercised.
Running example: specify checkout discounts
The prompt article introduces the request. Turn its confirmed product decisions into a versioned specification: pricing owns eligibility and calculation, web displays the returned quote, and orders persists the accepted quote. No-code checkout preserves current behavior.
Before implementation, resolve how long a quote remains valid, whether it can be reused, the rounding policy, and what happens when eligibility changes before submission. The following cases assume the team has decided that an expired quote requires a new quote and customer confirmation.
| Acceptance case | Setup and action | Independent evidence |
|---|---|---|
| Valid code | Seed an eligible cart and code; quote and submit | Displayed total and persisted amount match a reviewer-defined fixture expectation |
| Expired code | Seed a code expired at a controlled clock time | Agreed error appears; no discounted order is created |
| No code | Submit the existing checkout fixture | Existing total and behavior remain unchanged |
| Quote expires before submission | Advance the test clock beyond the agreed validity interval | Submission requests a new quote and confirmation; no order uses the expired quote |
| Cross-service amount mismatch | Exercise candidate web, orders, and pricing revisions together | UI, API response, and database assertion identify the same quote and amount |
Expected amounts must come from the agreed rules and fixtures. Copying the application’s calculation into a test oracle can reproduce the same mistake. Record a stable case ID in the specification, test, and run report so reviewers can trace each requirement to evidence.
Example: handling duplicate notification events
“Send a notification when an order ships” leaves delivery semantics unresolved. Here is a more reviewable specification for an illustrative consumer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Shipment notification consumer
## Goal
Send one notification for each shipment event, including when the broker
redelivers the event.
## Input and assumptions
- Input contains event_id, order_id, recipient, and shipped_at.
- The producer preserves event_id on redelivery.
- Validate input against the versioned event schema before sending.
- The notification provider accepts an idempotency key and retains it
for at least 7 days. Verify this guarantee before implementation.
## Behavior
- Use event_id as the provider idempotency key on every attempt.
- Persist successful delivery before acknowledging the broker message.
- A redelivery already marked successful is acknowledged without sending.
- Concurrent deliveries of an event must use the same idempotency key.
- Retry transient failures with backoff, for at most 24 hours.
- Route invalid events and exhausted retries to a dead-letter queue.
- Do not replay events older than the provider's deduplication window
without an explicit operator decision.
## Acceptance cases
- First delivery sends and records success.
- Redelivery after success does not send again.
- Concurrent deliveries do not produce duplicate provider sends.
- A crash after provider success but before local persistence retries
with the same key and does not duplicate the notification.
- Malformed events never reach the provider.
## Out of scope
Changing templates, adding channels, and bulk replay tooling.
The provider guarantee is a dependency to verify, not a fact established by writing it down. If the provider cannot deduplicate requests, the crash between sending and recording success becomes a product trade-off: possible duplicate sends or possible missed sends. Resolve that decision before promising one notification per event.
A specification is valuable when it exposes this uncertainty.
Map the delivery guarantee to explicit fault tests:
| Acceptance case | Fault or setup | Assertion |
|---|---|---|
| Redelivery after success | Deliver the same event twice | One provider effect; success remains recorded; second delivery acknowledged |
| Concurrent deliveries | Release two workers against the same event using a barrier | Both use the same key; deduplicating provider fixture records one effect |
| Crash after provider success | Terminate the worker after provider acceptance, before local persistence; redeliver | Retry uses the original key; one provider effect; success eventually persists |
| Invalid event | Supply an event that violates the schema | No provider call; dead-letter outcome recorded |
Instrument the provider fixture to count effects separately from requests: a retry may make another request without another notification. These tests verify the consumer against the simulated provider contract; confirming the real provider’s guarantee remains separate evidence.
For executable acceptance tests, also reference a versioned runbook with exact build and deployment commands, required services, readiness checks, application URLs, test roles, and database assertions. Record credential references and provisioning methods rather than password values. The loop-engineering post shows how to give agents end-to-end test access with scoped credentials and isolated environments.
Plan against the real repository
Ask the agent to locate the existing consumer, persistence layer, and test fixtures before proposing changes. A useful plan names affected components, describes the failure handling, and maps acceptance cases to checks.
If the tool provides a planning mode, verify its permissions rather than assuming the label means read-only execution. A written instruction to avoid edits is different from an enforced filesystem restriction.
Review depth should match impact. A local formatting fix rarely needs a separate approval stage. A change to delivery semantics or a shared contract benefits from review before implementation.
Quick code generation can raise maintenance costs
An agent can quickly generate interfaces, adapters, command handlers, mappings, and tests. That makes a pattern easier to implement, but it does not establish that the pattern is appropriate. Each abstraction becomes part of the system that future developers and agents must understand and change.
Even the same model may need to rediscover that structure in a later session. Its original conversation may be unavailable or incomplete, and the implementation may have evolved. Durable contracts, tests, and decision records help; having generated the code once does not remove the cost of understanding it again.
Review the implementation of the pattern
A pattern name alone does not reveal its cost. These are design trade-offs to examine, not a ranking of architectures by AI efficiency:
| Choice | When the boundary earns its place | Pitfall to inspect |
|---|---|---|
| Clean Architecture or ports and adapters | Domain rules need isolation from transport, persistence, or external systems | Pass-through layers and repeated mappings that add coordinated edits without isolating meaningful change |
| CQRS | Read and write models have substantially different responsibilities or requirements | Applying separate models to simple CRUD, then adding projection infrastructure without a demonstrated need |
| Event sourcing | Event history and reconstruction requirements justify the model | Treating event evolution, replay, and projection recovery as free because event handlers were easy to generate |
| Interfaces and dependency injection | A real boundary needs substitution or independent testing | An interface, factory, and registration for every concrete type regardless of consumer needs |
For more on evaluating architectural trade-offs and recording decisions when there is no universal best practice, see Chapter 1 of Software Architecture: The Hard Parts.
Clean Architecture emphasizes dependency direction and separation of concerns; it does not prescribe an identical number of layers for every application. See Robert C. Martin’s original explanation.
CQRS separates command and query models. It does not inherently require event sourcing, separate databases, or asynchronous messaging. Those additional choices introduce their own costs. Martin Fowler’s CQRS discussion also warns about applying it where its complexity is not justified.
Test the next change, not just the initial generation
Consider an illustrative request to add an optional delivery note to an order. In one design, the change belongs in request validation, the order model, persistence, and tests. An unnecessarily elaborate implementation might also require a command object, handler, mapper, interface registration, event schema, and read projection. This is a hypothetical comparison, not a benchmark or a list of components required by CQRS.
The extra structure can increase the work of finding affected files, keeping representations aligned, regenerating fixtures, and diagnosing partial updates. More retrieved context and repeated repair attempts can also increase model usage. Conversely, a well-chosen boundary can contain the change and reduce that work. Fewer files is not automatically better if it means duplicating rules or spreading dependencies through the application.
Before accepting a substantial architectural addition, try a representative follow-up change in an isolated prototype. Compare alternatives with equivalent behavior and verification requirements. Record model and tool configuration, repeat tasks where practical, and examine:
- Whether the change is correct, including missed consumers and regressions.
- Files inspected and changed, tool calls, and repair attempts.
- Input/output usage, cache usage, billed cost, and elapsed time.
- Human review effort and any added deployment or recovery work.
This measures maintenance more directly than how quickly an agent produces the first scaffold. Do not remove useful boundaries from an existing system merely to reduce token counts; account for migration risk and the requirements those boundaries satisfy.
Make architectural restraint part of the plan
Add a review requirement such as this to a substantial feature’s specification:
1
2
3
4
5
6
7
Follow the existing architecture and target-language conventions.
Before introducing a new layer, interface, queue, or read model:
- Identify the concrete requirement it satisfies.
- Compare it with the simplest compatible implementation.
- Trace one likely follow-up change through both designs.
- Explain testing, rollout, and ongoing maintenance consequences.
Record the decision and the conditions that would justify revisiting it.
AI can help explore and challenge these alternatives. The acceptance decision should depend on the system’s actual constraints and the team’s ability to maintain it, including with future agent sessions.
Keep the process iterative
Specifications are compatible with Agile development. The Agile principles explicitly value technical excellence, frequent delivery, and responding to changing requirements.
A small specification can evolve as a prototype or test reveals missing information. Avoid treating a task as a one-way “micro-waterfall” where an early assumption becomes untouchable.
flowchart TD
accTitle: Specification and implementation feedback
accDescr: Specify behavior, plan, review decisions, implement an increment, and verify acceptance cases. Defects return to implementation; requirement gaps return to specification. Deliver when evidence supports the change.
S["Specify behavior"] --> P["Plan against the code"]
P --> R["Review consequential decisions"]
R --> I["Implement a small increment"]
I --> V["Verify acceptance cases"]
V -- "Implementation defect" --> I
V -- "Requirement gap" --> S
V -- "Evidence supports the change" --> D["Deliver and observe"]
Keep the specification alongside the code when it records an ongoing contract. Update both when behavior changes, and retain the reason for consequential decisions.
Keep maintained prompts synchronized with intent
Wei Zhang and Jessie Jie Xia’s Structured-Prompt-Driven Development treats reusable prompts as versioned team artifacts and explicitly synchronizes them with code. Adopt that practice when a prompt will guide later changes; a disposable request does not need its own permanent specification.
For checkout, a requirement change to quote expiry should update the feature specification, reusable implementation prompt, and acceptance cases in the same review. A refactoring that changes the pricing client’s location should update any maintained references to it. Preserve the reason for a behavioral decision, rather than retaining a transcript of every generation attempt.
Synchronization must not turn an implementation mistake into a requirement. If code accepts expired quotes, compare it with the agreed policy before changing the specification to match. Record deliberate policy changes separately from repairs. Prefer links to authoritative contracts and tests over copies of details that will drift.
Constrain repeated work with domain vocabulary
Unmesh Joshi’s DSLs Enable Reliable Use of LLMs describes using small domain languages and validators to narrow the model’s choices. The useful distinction is between discovering a domain model through experiments and generating new cases within an established model.
If checkout scenarios recur, a team could define this illustrative test vocabulary:
1
2
3
4
5
6
scenario: expired-quote
given: eligible-cart-with-valid-quote
when: advance-clock-past-quote-expiry-and-submit
expect:
- requote-required
- no-order-created
This YAML is not an executable test by itself. A maintained runner must validate allowed steps, reject unknown names, and implement their semantics. Independent fixtures must establish the expected behavior. A parser accepting the scenario does not prove that the runner or application is correct.
Start with existing test helpers or typed domain APIs. Create a custom language only when repeated scenarios justify maintaining its vocabulary, validator, and execution engine. Keep unfamiliar product behavior open to experimentation before encoding it as a fixed operation.
Spend planning effort where it reduces uncertainty
Not every failure comes from underspecified requirements. Lack of context, incorrect tool use, dependency failures, and implementation mistakes also matter. Planning has a cost, so use it to answer concrete questions rather than producing paperwork for every edit.
The useful outcome is a change whose behavior, assumptions, and verification are clear to a reviewer. The final post connects that specification to execution: Loop Engineering.