Spec-Driven Development: A Practical Guide from PRD to Code
You open Claude Code, describe the feature in three lines, and hit run.
It hands you 400 lines. It compiles. The tests (which it wrote itself) pass. You merge.
Two weeks later, support opens a ticket: the sales report PDF is adding canceled orders to the total, the file name has no date in it, and a sales rep can see another rep's revenue. The agent didn't make anything up. You simply never told it those rules existed. That is exactly the hole Spec-Driven Development exists to close.
This scene has a name now. It's what the GitHub folks called vibe coding when they launched Spec Kit in 2025: asking AI for a feature and assuming it will guess context that lives only in your head. Spec-Driven Development (SDD) is the answer. The spec stops being a PR comment and becomes executable input: the AI generates code from it, not from your short prompt.
In this tutorial we run the full Spec-Driven Development cycle in Laravel, with a feature that looks trivial on purpose (exporting a sales report as a PDF) to show that SDD earns its keep precisely on the ones that look silly. There will be a PRD, spec.md, plan.md, tasks.md, generated code, a Pest test, and the point where the human still calls the shots. PHP stack, which is the niche nobody covers in this conversation.
Where to start with Spec-Driven Development
Spec-Driven Development isn't a single method, it's a spectrum. At the light end, one page with a goal, constraints and acceptance criteria, which fits in a commit body. At the heavy end, a constitution.md with architecture rules, a spec.md per feature, a plan.md with endpoint contracts, and the agent executing task by task with review in between. Picking the wrong point on the spectrum costs you in both directions: too little spec and the agent invents business rules; too much spec and you spend four hours writing markdown for a two-hour feature.
The rest of this post is the full cycle, end to end, on a real Laravel case. If you got here with a more specific question, start with the item that describes your situation:
- My project is legacy and SDD looks like a greenfield thing. You can flip the flow around: the agent reads the existing module, generates a reverse spec, you review it, and from there the normal cycle runs. The path is in from legacy to SDD: reverse spec on a legacy module.
- I already run SDD and want to know whether BMAD adds anything. BMAD is Spec-Driven with personas and more ceremony. Where that pays for its overhead and where it just delays delivery is in BMAD-Method for people already using SDD.
- I suspect it has turned into overengineering. The honest critique of the method (reading cost, double review, drift between spec and code, false sense of control) is in the specification paradox.
- The spec grew, got approved by committee and nobody reads it anymore. The symptoms can be spotted before they become culture. Diagnosis and fix in 5 signs your specification has turned into bureaucracy.
- I'm still choosing between vibe coding, agentic code and SDD. The eight decision criteria and a tree ready to paste into your team wiki are in agentic code vs vibe coding vs SDD.
- The spec is already solid and the bottleneck is now the agent itself. The spec settles what the agent builds; architecture settles what it is, and that's the part we build live at the AI Engineering Lab, September 19 and 20.
The rule of thumb I use to decide how heavy to go: add up the cost of getting the feature wrong. If the bug means half an hour of rework, don't write a spec, write the test. If it means a wrong number in a report that goes to accounting, undercharging a customer's card, or leaking another user's data, write the whole spec and treat every acceptance criterion as a contract. The example in the rest of this post, exporting a sales PDF, falls into the second group precisely because it looks like the first: it has role-based permissions, it has LGPD (Brazil's data protection law, the local equivalent of GDPR), and it has a total that can't include canceled orders.
What is Spec-Driven Development?
Spec-Driven Development (SDD) is a way of building software with AI in which a structured specification is written before the code and becomes the project's source of truth. Instead of the agent generating code from a short prompt, it generates from a versioned spec with explicit requirements, acceptance criteria and constraints. The typical cycle has four phases: specify → plan → break into tasks → implement with verification.
In practice, SDD is the antidote to vibe coding: fewer hallucinated business rules, more traceability. If you're still deciding which approach to use in each situation, we compared the three in agentic code vs vibe coding vs SDD: decision table.
TL;DR
- What it is: SDD means writing the specification before the code when you program with AI. The spec is the project's source of truth, not the prompt.
- Stack/Models: Laravel 11, Pest 3, dompdf, Claude Code, GitHub Spec Kit (or a custom variation like Pimzino claude-code-spec-workflow).
- Cost/Access: Spec Kit is open source. Claude Code requires a subscription. Everything in this post runs locally.
- Repository/Useful link: github.com/github/spec-kit and Birgitta Böckeler's reference essay at martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html.
The context: why SDD is on the agenda now
In 2025 GitHub published Spec Kit, AWS launched Kiro, Tessl pushed the frontier toward "spec-as-source", and Thoughtworks published the most lucid essay on the subject. In 2026, IBM and Microsoft joined the conversation with their own guides and Spec-Driven Development became an emerging standard: searches for the term in Brazil grew a thousandfold in one year.
The thesis, from GitHub's own spec-driven.md, is blunt:
"Specifications don't serve code — code serves specifications. The PRD isn't a guide for implementation; it's the source that generates implementation."
In practical terms: the requirements document stops being a dead PDF in the /docs folder and becomes input the agent reads. The official Spec Kit cycle is Constitution → Specify → Plan → Tasks → Implement, all /speckit.* commands in Claude Code, generating versionable files in .specify/specs/[feature]/.
The toolkit has grown quite a bit since the announcement, and it's worth knowing what exists today before you hand-roll your own flow. Beyond the main commands, Spec Kit now ships /speckit.clarify, which forces the agent to close the spec's ambiguities before planning; /speckit.analyze, which cross-checks spec, plan and tasks looking for contradictions among the three; and /speckit.checklist, which generates verification lists by area. And it's no longer exclusive to Claude Code: specify init asks which agent you use and writes the commands in its format, which matters if your team isn't all on the same tool.
But SDD is no silver bullet. Birgitta Böckeler of Thoughtworks herself points out two pains that are unlikely to go away:
"I'd rather review code than all these markdown files."
And:
"The agent ultimately not follow all the instructions."
Honest summary: SDD drastically reduces the odds of the agent hallucinating business rules, but it doesn't eliminate the review work or the non-determinism of the LLM. We pay for that pain in markdown. In return, we get traceability a short prompt never gives you.
And this is where the PHP ecosystem gets left out of the conversation. The public SDD examples are almost all in Python, Next.js or TypeScript. Laravel rarely shows up. Let's fix that.
Prerequisites
- PHP 8.3, Composer, Laravel 11.
- Claude Code installed (
npm install -g @anthropic-ai/claude-code). - Spec Kit via
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git, and thenspecify initat the project root (or run it command by command manually, as I'll show). The CLI isn't published on PyPI: installing straight from the repository is the supported path. - Pest 3 (
composer require pestphp/pest --dev). barryvdh/laravel-dompdffor PDF generation.
The whole structure shown below is clonable: copy the files inside .specify/specs/sales-report-pdf/ into a fresh Laravel project, open Claude Code, and the cycle runs.
A tutorial shows you the way — in the Clã you build alongside us. A live class every week, real AI Engineering projects, next to people already in production.
Join the ClãThe trivial feature with more edges than it seems
GET /reports/sales/export.pdf. Looks dumb. It isn't.
Here's what is implicit in a request like that and rarely makes it into the prompt:
- Date range filter. Default? "Last 30 days" or "current month"? In which timezone?
- Permission. Admin sees everything. A sales rep sees only their own orders. How does that tie into
User? - Order status. Do canceled orders go in the report? Returned? Drafts?
- Sensitive data. Is the customer's CPF (the Brazilian individual taxpayer ID) masked or raw? And what if legal has already come knocking about LGPD?
- Rendering. More than 5,000 rows, and doing it synchronously hangs the request. Does it go to a queue? Email with a link?
- File name.
relatorio.pdfbreaks the UX.vendas-2026-04-01-a-2026-04-30.pdfis what the user expects. - Cache. Does the same range requested twice in two minutes generate the PDF again from scratch?
Vibe coding this feature usually forgets two or three of those. SDD forces you to trip over them in the document, before the code.
Hands-on: the SDD tutorial applied
Step 1: the human PRD (what still needs people)
Before the agent comes in, someone writes the PRD. The focus is on outcome and business context, not technique. Spec Kit calls this "intent".
# PRD: Sales report export as PDF
## Problem
The sales team needs to send a monthly sales report to
accounting by email. Today they export a CSV and format it by hand
in Excel. It takes 30 to 60 minutes a month, per manager.
## Desired outcome
An "Export PDF" button on the reports dashboard returns the document
in the company's visual standard, ready to send.
## Known constraints
- Only admin and manager can export.
- A sales rep only sees their own numbers and doesn't export.
- LGPD: the customer's CPF comes out masked (xxx.xxx.xxx-12).
- Canceled orders show up with a "canceled" badge and don't count toward the total.
- Default range: current month, in the America/Sao_Paulo timezone.
## Non-goals
- It's not an interactive chart. It's a static PDF.
- It's not a scheduled export. It's on demand.
This file is the raw input. It has no technical decisions: no "use dompdf", no "REST endpoint". That's the job of /plan.
Step 2: /specify, turning it into an executable specification
In Claude Code, inside the Laravel project:
/speckit.specify
You paste in the PRD. The agent returns a structured spec.md at .specify/specs/sales-report-pdf/spec.md. The difference is that it makes explicit the ambiguities the PRD was hiding:
# Spec: Sales report export as PDF
## User Stories
US-1: As a manager, I want to export the current month's PDF in 1 click.
US-2: As an admin, I want to filter by an arbitrary range before exporting.
US-3: As a sales rep, I want to export only my own orders.
## Acceptance Criteria
AC-1: The generated PDF contains: header with logo, date range, gross total,
net total (excluding canceled), orders table.
AC-2: Canceled orders appear in the table with a visual badge,
but are not part of the net total.
AC-3: Customer CPF rendered in the format xxx.xxx.xxx-NN.
AC-4: File name: "vendas-{YYYY-MM-DD}-a-{YYYY-MM-DD}.pdf".
AC-5: A sales rep who tries to export a range outside their own gets a 403.
## [NEEDS CLARIFICATION]
- NC-1: Above how many rows does generation go to the queue?
- NC-2: Is the PDF cached? For how long?
- NC-3: Automatic email with a link after async generation, or download only?
Those [NEEDS CLARIFICATION] items are the gold of SDD. The agent flags what it can't infer on its own. You answer, update the spec, and move on. Without that, it invents a default.
Step 3: /plan, choosing stack and contracts
Now the technical part comes in. Command:
/speckit.plan
Output at .specify/specs/sales-report-pdf/plan.md:
# Plan: Sales report export as PDF
## Stack
- Renderer: barryvdh/laravel-dompdf (enough for the simple layout).
- Action class: App\Actions\Reports\ExportSalesPdf.
- Controller: thin, only validates and calls the action.
- View: resources/views/reports/sales-pdf.blade.php.
- Queue: Laravel Queue. Threshold = 1,000 rows.
## Contracts
- Endpoint: GET /reports/sales/export.pdf
- Query: ?from=YYYY-MM-DD&to=YYYY-MM-DD
- Synchronous response: application/pdf, 200.
- Asynchronous response: 202 + { job_id, poll_url }.
## Policies
- SalesReportPolicy::export: checks role + sales rep scope.
## Recorded decisions
- dompdf chosen over Snappy: the layout doesn't use advanced CSS3.
- Cache: 5 minutes per (user_id + from + to).
Notice: plan.md is where technical decisions live versioned, not in a PR comment nobody can find later.
Step 4: /tasks, breaking it into testable tasks
/speckit.tasks
Output in tasks.md:
# Tasks
T1. Create a composite index migration (user_id, status, created_at) on orders.
T2. Create SalesReportPolicy with an export() method.
T3. Create the ExportSalesPdf Action with __invoke(User $user, CarbonPeriod $range).
T4. Create SalesReportPdfRequest with from/to validation.
T5. Create SalesReportController@export, thin.
T6. Create the PDF Blade view with CPF masking.
T7. Create the ExportSalesPdfJob Job for volumes above the threshold.
T8. Pest: feature test for AC-1, AC-2, AC-3, AC-4, AC-5.
T9. Pest: unit test for CPF masking.
T10. Document the route and params in the README.
Each task is small, testable, and tied to an AC. This is where the gain shows up: instead of the agent spitting out 400 lines at once, it delivers T1 → review → T2 → review.
Step 5: /implement (with a human brake)
/speckit.implement T8
Before any line of application code, the agent writes the Pest test for T8. To make it worth it, ask it to run the test before implementing, which confirms it fails for the right reason:
<?php
use App\Models\{User, Order};
it('exports the PDF with correct totals ignoring canceled orders (AC-2)', function () {
$admin = User::factory()->admin()->create();
Order::factory()->count(3)->for($admin)->create(['status' => 'paid', 'total' => 100]);
Order::factory()->for($admin)->create(['status' => 'canceled', 'total' => 999]);
$response = $this->actingAs($admin)
->get('/reports/sales/export.pdf?from=2026-04-01&to=2026-04-30');
$response->assertOk();
$response->assertHeader('content-type', 'application/pdf');
$pdfText = (new \Smalot\PdfParser\Parser())
->parseContent($response->getContent())->getText();
expect($pdfText)
->toContain('Total líquido: R$ 300,00')
->not->toContain('R$ 999,00');
});
Did it fail? Good. Now the agent implements the Action:
<?php
namespace App\Actions\Reports;
use App\Models\{Order, User};
use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\CarbonPeriod;
final class ExportSalesPdf
{
public function __invoke(User $user, CarbonPeriod $range): string
{
$orders = Order::query()
->whereBetween('created_at', [$range->start, $range->end])
->when($user->isVendedor(), fn ($q) => $q->where('user_id', $user->id))
->orderBy('created_at')
->get();
$totalLiquido = $orders->reject->isCanceled()->sum('total');
return Pdf::loadView('reports.sales-pdf', [
'orders' => $orders,
'totalLiquido' => $totalLiquido,
'range' => $range,
])->output();
}
}
Run Pest again: it passes. Next task. That's the loop. Each task lands in a commit with a reference to the AC and the spec, which is traceability a free-text PR description never delivers. Want to see how this cycle behaves on a real feature, with the entire PR coming out of the agent? We documented the experience in hands-on: a PR 100% generated by an agent in Laravel.
Limitations and caveats (the part nobody posts in the thread)
1. Markdown sprawl. Four files per small feature. On a project with 200 features, it becomes hell. Solution: SDD isn't for every CRUD. Apply it to features with business rules: reports, integrations, approval flows. A migration for a new column doesn't deserve a spec.md.
2. The agent drifts even with a rigid spec. I've seen a spec saying "CPF mask xxx.xxx.xxx-NN" and the code come out with "***.***.***-NN". That's not a failure of the method, it's the non-determinism of the LLM. Pest is the safety net: without tests, SDD is just bureaucracy.
3. The constitution is fragile. Spec Kit proposes a constitution.md with immutable rules ("every Action is final", "controllers are thin"). It works on a new project. On a 5-year-old legacy codebase, the constitution only holds where you enforce it. You can't apply it retroactively, but you can extract a reverse spec from an existing module and bring the legacy code into the cycle little by little; we show the path in from legacy to SDD: reverse spec on a legacy module.
4. LGPD and sensitive data. The spec must say explicitly what comes out masked. If you forget, the agent exposes it. Treat this as a first-class acceptance criterion, not an implementation detail.
5. It's not a silver bullet. A feature with 1 SQL query and zero business rules is overhead. SDD wins where the cost of error is high: financial reports, gateway integrations, payment flows, exports that go to accounting.
FAQ
Does it work without the official Spec Kit?
Yes. Spec Kit is just a folder convention plus /speckit.* commands in Claude Code. You get the same thing with a custom .claude/commands/specify.md, plan.md and tasks.md. Pimzino's claude-code-spec-workflow is a ready-made alternative you can drop into your project. And if you already run SDD and want to compare it with another structured methodology, we broke down the BMAD Method for people already using SDD. What matters is the cycle, not the name of the command.
Do I need to redo the whole project? No. SDD is per feature. Legacy stays as it is. Every new feature goes through the cycle, and in 6 months half of the code that matters is already versioned with a spec.
Do Pest tests replace the spec? No. The spec is "what" and "why". The test is "how do I verify it works". Different layers. When someone asks why the canceled order doesn't count, the spec answers. When someone wants to know whether it still works after the refactor, the test answers.
Can dompdf handle a large report?
For a simple tabular PDF, yes: up to around 2,000-3,000 orders it renders fine. Beyond that, or if you have charts or advanced CSS3, switch to Browsershot (Headless Chrome). But that's a detail that lives in plan.md, not in spec.md.
Conclusion
Spec-Driven Development applied to Laravel isn't a change of framework, it's a change of input. The agent leaves guesser mode and enters executor mode: the spec, with explicit acceptance criteria, is what it reads to generate code. The extra markdown pays off in traceability, in [NEEDS CLARIFICATION] items that catch ambiguity before the PR, and in commits that tie code to business rules. What's left for the human is what it always was: think through the problem, decide the trade-offs, and review with judgment.
The next step after SDD is harness engineering: building the tooling around the agent so it runs in a real product, not a demo chatbot (if the term is new to you, start with what an AI harness is). That's exactly what I'll break down live at Do Prompt ao Harness ("From Prompt to Harness"), July 11 and 12: two days building a real sales agent from scratch, with Claude Code and Laravel, no magic prompt and no demo chatbot.
Sources:
{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.
There is no shortage of content. What is missing is someone to untangle it: what matters now is how to implement it the right way. In the Clã you get that live, every week, with people who have already filtered out the noise.
Join the Clã