DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on Originally published at stacknotice.com

Spec-Driven Development with Claude Code (2026): Write the Spec, Ship the Code

There's a pattern separating developers who get consistently good output from Claude Code and those who keep fighting it: the ones who get good output write the spec first.

Not a prompt. A spec. There's a difference.

A prompt is "build me a user authentication system with email and password." A spec is a structured document describing what the system needs to do, what it connects to, what the edge cases are, and what done looks like. One produces a generic scaffold. The other produces code that fits your actual project.

Why Prompting Alone Breaks Down

When you prompt without context, Claude fills the gaps with assumptions. Reasonable ones — but not yours.

"Add a checkout flow" produces Stripe integration. But maybe you're on Paddle. Maybe checkout needs to hit three internal services. Maybe you have a custom pricing model. Claude doesn't know.

The result: three rounds of corrections steering output toward what you actually wanted. Fix the input, not the output.

What a Spec Looks Like

# Feature: User Subscription Management

## What it does
Users can subscribe to plans (free, pro, enterprise), upgrade/downgrade,
and cancel. Billing through Stripe. Upgrades take effect immediately,
downgrades at end of billing period.

## Data model
- User has one active Subscription
- Subscription: planId, status, stripeSubscriptionId, currentPeriodEnd
- Existing: users table (Drizzle + Postgres), auth via Better Auth

## Business rules
- Free plan: no credit card required
- Cancellation: user keeps access until currentPeriodEnd
- Webhook events: checkout.session.completed, subscription.updated, subscription.deleted
- Failed payments: email after 3 attempts, downgrade to free

## What NOT to build
- No trial periods (adding later)
- No team/seat billing (separate feature)
- No invoice history UI (use Stripe portal)

## Done looks like
- Free → pro in under 30 seconds
- Webhook handles duplicate events without duplicate records
- Cancel shows when access expires, not just "cancelled"
Enter fullscreen mode Exit fullscreen mode

This takes 20-30 minutes to write. It saves 2-3 hours of iteration.

The Workflow

Step 1: Spec in CLAUDE.md or specs/[feature].md

For project-wide context, CLAUDE.md. For a specific feature, a dedicated spec file you reference explicitly.

Step 2: Plan Mode before any code

/plan

Read specs/subscription-management.md and tell me:
1. What files you'll create or modify
2. What the webhook handler will look like
3. What migrations are needed
4. What you'll skip and why
Enter fullscreen mode Exit fullscreen mode

If Claude's plan doesn't match your mental model, the spec is underspecified. Fix the spec, not the code.

Step 3: Execute in sections

Implement the database schema from the spec.
Use Drizzle. Don't touch the API layer yet.
Enter fullscreen mode Exit fullscreen mode

Review. Then add the endpoint. Review. Then the webhook. Section by section, not all at once.

Step 4: Use the spec as the test oracle

Generate Vitest tests from specs/subscription-management.md.
Each business rule should have at least one test.
Focus on webhook idempotency and cancel flow timing.
Enter fullscreen mode Exit fullscreen mode

A Reusable Spec Template

# Feature: [Name]

## Context
[What exists, what this builds on, what the user need is]

## What it does
[Bullet list of capabilities from the user's perspective]

## Technical constraints
[Existing stack, services in use, things not to change]

## Data model
[New tables/fields, changes to existing]

## Business rules
[Non-obvious logic — edge cases, timing, error states]

## What NOT to build
[Explicit exclusions]

## Done looks like
[Observable, testable acceptance criteria]
Enter fullscreen mode Exit fullscreen mode

Specs for Bugs Too

# Bug: Dashboard shows wrong totals with date filter

## Current behavior
"Last 30 days" totals don't match raw DB query. Off by ~3-5%.

## Expected behavior
Match: SELECT SUM(amount) FROM orders WHERE created_at >= NOW() - INTERVAL '30 days'

## What I've checked
- Query in analytics.ts line 84 looks correct
- Issue only with date filter, not "All time"
- Started after timezone migration on 2026-08-15
- Postgres: UTC, app: Europe/Madrid

## Hypothesis
Date filter applies timezone offset twice.

## What I need
Find where the double offset happens and fix it.
Don't change chart rendering logic — only query/date handling.
Enter fullscreen mode Exit fullscreen mode

When Specs Don't Help

  • Genuine exploration — you don't know what you want yet. Free-form prompting is fine.
  • Trivial tasks under 10 minutes. Overhead isn't worth it.

My threshold: if implementing this wrong would cost more than an hour to fix, write a spec.


The cognitive shift: you're doing the hard thinking before code exists, not after. Claude is capable of building complex systems correctly. The limiting factor is usually underspecified input.

Specs fix the input.


Full article: stacknotice.com/blog/spec-driven-development-claude-code-2026

Top comments (0)