What a spec is, four ways teams keep them alive, and how the lifecycle plays out across a multi-repo project
Spec-Driven Development is the practice of writing a structured specification — describing intent, scope, and constraints — before or alongside the code that implements it. The spec is the contract; tests and code are how you keep your end of it.
A spec separates what we agreed to build from what's currently in main. Without that separation, the only source of truth is the code — and "what we meant to do" gets lost the moment a contributor leaves or a feature evolves.
A good spec template has fixed sections so readers always know where to find a given piece of information. The seven below are the ones that show up across most SDD templates.
| Section | What goes here |
|---|---|
| Why | 1–2 sentences. The problem this solves and why now. |
| What | The concrete deliverable, specific enough that you'd know when it's done. |
| Constraints | Three subsections: Must (required patterns/libraries), Must Not (forbidden — e.g., no new deps), Out of Scope (adjacent work explicitly not included). |
| Current State | What already exists. Saves the next contributor from re-exploring the codebase. |
| Tasks | The work broken into discrete units. Each task lists What, Files, and Verify (the command or check that proves it's done). |
| Validation | End-to-end verification once all tasks are complete — automated suites, manual UI checks, cross-feature dependencies. |
Each task gets a one-line proof that it works — usually a single test command. The aggregate of all Verify lines tells you, at a glance, which parts of the feature have automated coverage and which don't. The Validation section is the cumulative version: "if all tasks are green, here's how I check the whole feature still hangs together."
Once a spec exists, what happens to it as the code changes? Real teams pick (or drift into) one of these four patterns.
The spec is kept in sync with the code. PRs that change behavior also update the spec.
The original spec is preserved as a record of intent at time of build. New scope means a new spec, or an "Amendment N" appended at the bottom of the existing one.
Append-only log of decisions. Each new decision gets its own short document; if it supersedes a previous one, the old ADR is marked "Superseded by ADR-N" but never edited.
The spec is a planning artifact, used once during implementation and then ignored. Code, tests, and READMEs become the source of truth.
The parts of a spec that rot fastest are the test-coverage claims. Two conventions keep them honest.
Verify — pick one of four| Wording | When to use |
|---|---|
<command to run> | There's a dedicated test for this task. |
No dedicated test; indirectly covered by <command>. | The behavior is exercised under a related test (e.g., a repository tested through its service). |
No tests cover this task yet. | A test is missing but warranted. |
No tests — type-only, intentionally not covered. | For type definitions or trivially presentational components. |
Validation section — three sub-blocksnpm test, ./mvnw test) plus the feature-specific test files.Constraints rarely lie — they're upstream of the work. The What rarely lies — it's broad. But "this is covered by test X" is the kind of claim that becomes wrong the moment a test is renamed, removed, or never written. Forcing every task to declare its verification status, in one of four standard wordings, makes the lying visible.
Three repos, one spec library. Each repo has a different test runner and CI; the specs live separately so they describe both layers without belonging to either.
jwt-auth-spec.md, profile-spec.md, practice-log-spec.md, etc. Plus a spec.example.md template. No code.
npm test). Lint and tests run in GitHub Actions; deploys to Vercel on merge to main.
./mvnw test). Test classes target services and controllers; commands like ./mvnw test -Dtest=AuthServiceTest run a single suite.
Whatever kind of change goes through the same loop: create a feature branch, work in the relevant repo, open a PR, then update the spec after the PR merges to main.
| Change type | What to update in the spec |
|---|---|
| New feature | Either a new spec file or a new task block in an existing one. Move related items out of Out of Scope. Add Verify + extend Validation. |
| Bug fix | Usually no change — the bug means code disagreed with spec. If the spec was ambiguous about the case, tighten Constraints. Add a Verify if a regression test was added. |
| New tests | Update Verify for any task whose status changes from "no tests yet" to a real command. Add the new test file to the Validation section. |
| Behavior change | Update What and Constraints. Add an entry to Current State if the change leaves an intentional gap (e.g., "service still calls findAll() instead of the sorted query"). |
Updating the spec inside the same PR sounds tidier, but in practice the spec change ends up reviewed alongside code review, where reviewers focus on the diff. Keeping it as a follow-up — in the docs repo — lets the spec change get its own focused review and avoids holding code merges hostage to documentation nits.
A spec is only useful if you can trust it. The four lifecycle patterns are really four answers to the question "who keeps this honest, and how?" — and for any non-trivial project, picking that answer up-front matters more than which template you use. Living specs need Verify and Validation as the discipline mechanism; ADRs need supersession rules; frozen specs need a clear amendment process. No mechanism, no trust.