Writing
Spec-driven development that actually sticks
Why I write the contract before the code, how I set up spec-kit on existing repos, and the habits that keep the spec as the source of truth.
Most "design docs" die the moment implementation starts. The code drifts, the doc goes stale, and six months later nobody trusts either one. Spec-driven development (SDD) fixes that by making the spec the artifact you *build against* — not a thing you write once and abandon.
Here's how I actually do it, and why it holds up under real delivery pressure.
Contract-first, always
I design the API contract before I write the code. For a REST service that means an OpenAPI spec; for a GraphQL service it's the schema. The rule is simple: the spec, the entities/DTOs, the examples, and the generated classes stay in lockstep. The spec is the source of truth, and everything else conforms to it.
Concretely, my order of operations for a new endpoint is:
- Model the domain entities as JSON schemas first — with constraints and examples.
- Define the operation in the spec, referencing those schemas via
$ref, and add both success and error examples. - Conform to the internal API style guide and lint the spec.
- Generate Java classes, then rename them to match naming conventions if needed.
- Only then implement controller → service → repository.
The payoff is that by the time I write code, all the hard decisions — field names, constraints, error shapes — are already made and reviewed.
Standardize the boring parts
Two things I standardize aggressively:
- Errors. Every error response uses
application/problem+json(RFC 7807/9457) with consistenttype/title/detailfields. Clients get one machine-readable error contract instead of a zoo of shapes. - Constraints.
minLength/maxLength,minimum/maximum,required,format— declared in the schema, not buried in validation code.
components:
schemas:
ShareRequest:
type: object
required: [resourceId, recipientId]
properties:
resourceId:
type: string
minLength: 1
maxLength: 64
recipientId:
type: string
format: uuidWhen the constraints live in the contract, the generated code and the docs and the examples all agree by construction.
Setting up spec-kit on an existing repo
Adopting SDD on a greenfield project is easy. The interesting case is an existing repo with its own conventions. A few things that made it stick:
- Author a "constitution." A short, project-level document that states the non-negotiables: constructor injection, layered architecture, problem+json errors, maximal test coverage. Agents and humans both read from it.
- Freeze the plan during implementation. For anything large, I write the plan first, get it right, and then *don't edit it mid-flight*. If reality diverges, that's a signal to stop and re-plan — not to quietly rewrite history.
- Lint the spec in CI. A spec that isn't linted will drift. Make the linter a gate.
The plan is the contract. The code conforms to the plan. If you're editing the plan to match the code, you've inverted the relationship.
Why it holds up under pressure
The real test of a workflow is a deadline. SDD survives because it front-loads the ambiguity: the argument about field names and error codes happens in a spec review, where it's cheap, instead of in a production incident, where it's expensive.
It also composes with everything else I care about. Contract-first pairs naturally with test-coverage discipline — the spec tells you exactly what behaviors to cover — and with evidence-first debugging, because when something breaks you can diff the runtime behavior against a precise, written expectation.
If you take one thing from this: write the contract first, and then refuse to let the code quietly become the new source of truth.