What Actually Goes In a Spec
Everyone tells you to write one. Almost nobody tells you what to put in it.
Spec-driven development has had a good 18 months. Write the spec first, let the agent build from it, review the spec instead of the diff. I agree with all of it. I have been doing a version of this since February and it works.
Here is the part nobody covers: what goes in the spec.
Go and read the field. Kiro keeps project conventions in steering files. Spec Kit emits structured markdown at each phase of a gated workflow and marks gaps with [NEEDS CLARIFICATION]. Both are good tools. Both ship a schema as a product default instead of arguing for one. The furthest the public conversation gets is “use JSON Schema or Zod so the agent stops hallucinating your API shapes,” and that is the contract layer. Payloads and interfaces. It says nothing about actors, journeys, or the questions nobody has answered yet, which is exactly where design intent goes missing.
I think the gap exists for a boring reason. The vocabulary for this lives in a different tribe. Actors, entities, journeys, rules, constraints is domain modelling lineage: DDD, use-case models, Jackson’s problem frames. The people writing about spec-driven development are devtool vendors and DevOps practitioners who think in workflow phases (specify, plan, implement, verify). Phases generalise across every project and are easy to write about. A field list is a position, and a position invites an argument.
So here is my position, and how it has changed as the projects got bigger.
The 6 sections, and where each one came from
The artifact I write is a single typed file I call the intent model. It has 6 sections. I chose the set and I have been developing it since February, but every individual field traces back to a tradition much older than any of this.
| Section | Where it comes from | What I took |
|---|---|---|
| Actors | DDD, EventStorming | Who acts on the system, named and typed, not a persona |
| Entities | DDD tactical patterns | Entities, aggregates, invariants, and the idea that identity persists through change |
| Journeys | Cockburn’s use cases, Kruchten’s 4+1 “+1” | The scenario as the thing that ties the other views together |
| Business rules | DDD invariants, EventStorming policies, EARS | A rule as a named, testable assertion rather than a sentence in a paragraph |
| Constraints | Volere, ISO 25010 | Non-functionals as a first-class category |
| Open questions | EventStorming hotspots, the pink stickies | Unresolved ambiguity gets a slot in the artifact instead of a comment thread |
That table is the whole “isn’t this just X?” answer, at the field level rather than the methodology level. The synthesis is the point: 4 traditions that have never shared a file, typed into one artifact, with everything downstream generated from it.
Two of those 6 turned out stronger than I knew when I picked them. I checked the 6 against roughly 30 requirements frameworks, from DDD through to Spec Kit, and business_rules is the best-founded field in the set: GUIDE action assertions, Gherkin’s Rule keyword, SBVR, DMN and Zachman’s Why column all land on it independently. open_questions was the surprise. Only about 5 of the 30 carry anything like it. Volere has Open Issues and a Waiting Room, EventStorming has hotspots, Example Mapping has the red card, Cockburn has Open Issues, Spec Kit has [NEEDS CLARIFICATION]. Kruchten, RM-ODP, Zachman, C4, DMN, SBVR and BMM have nothing at all. A field for “we have not decided this yet” puts the file ahead of most published standards, which is not something I expected to be able to say.
What I left out on purpose
The useful half of any field list is the half you can’t see. A field list that only ever grows is a field list nobody has had to defend, so here is what I kept out and what each exclusion is worth.
Glossary and personas. Out, and they stay out. Entity and actor IDs are the ubiquitous language if you are disciplined about naming, which makes that a naming convention rather than a field. Personas are real and they belong in the design system, but they do not drive code, and anything in this file that does not drive code is weight the agent has to read past.
Invariants as their own section. Out. They already live in entity field constraints and rule severity, and a second home for the same idea is how 2 versions of a rule end up disagreeing.
Bounded contexts. Out in April, and that was right for the projects that existed then: one app, one file. The mail app changed the maths. Mail plus calendar plus contacts plus delegation plus search plus attachments plus admin in a single file, and generation started pulling constraints from one domain into a screen in another. Classic lost-in-the-middle. Splitting into 10 files by domain and injecting only the relevant slice per prompt fixed it outright, and it is now the default above a certain size. A call that held for 2 months and then got upgraded by scale is roughly what you want from a schema decision.
Read models. The open one, and the one I am most interested in. Between “an actor wants to do this” (a journey step) and “this entity has these fields” (an entity), nothing yet declares what the actor needs to see in order to decide. EventStorming gives that its own colour for good reason. Without it, an AI building a screen infers the read model from the action text and gives you every field on the entity, in schema order, in a form. Adding it is the next thing that pays for itself.
Decisions. open_questions[].decision already captures anything that was once a question. What has no home yet is the decision that was never a question (“Zustand over Redux, because”). Since the file takes the position that the spec is the truth, that trail is worth having, and it is a section rather than a rewrite.
Additive gaps like read models cost a screen. The structural one, bounded contexts, cost 2 months and taught me more.
Two projects, 2 completely different files
The booking system and the mail app use the same methodology and produce artifacts that barely resemble each other. That comparison taught me more than either project on its own.
| Vehicle booking system | Mail app | |
|---|---|---|
| Domain | Port logistics, shipments and slot booking | Mail, calendar and contacts suite |
| Shape | 1 TypeScript file, 157 KB | 10 JSON files, one per domain, about 2 MB |
| Carries | 4 actors, 17 entities, 16 journeys, 37 rules, 50 open questions | All of that plus 97 screens and 264 mutation contracts |
| Who consumes it | Design and backend both generate from it | Design generates from it. Engineering does not. |
| Reviewed by | Humans, section by section, with client sign-off | Tooling, schema lint and audit on every commit |
| Fails when | Something is missing, because the gap gets invented at generation time | Nothing fails, and that is the problem |
That last row is the finding. Birgitta Böckeler has a piece on Martin Fowler’s site that names 3 positions: spec-first, spec-anchored, spec-as-source. The booking system is spec-as-source. The model is authoritative, code on both sides of the stack is downstream output, and a wrong entity produces wrong artifacts and something breaks loudly. The mail app is spec-anchored, scoped to one discipline. Design consumes the model, engineering builds from its own reading, and the 2 sources of truth agree by convention rather than by generation.
Drift in the second shape is silent. Nothing errors. The model and the codebase just gradually describe different products, and you find out in month 5 when a designer hands over a screen for a state the app no longer has.
Which means the 2 projects need different content in the same file, and not because one is bigger:
- The booking system needs completeness. Whatever is missing gets invented at generation time, so exhaustive typing pays for itself.
- The mail app needs reconcilability. More rationale, more open questions, more provenance, less exhaustive typing, because the job is checking the model against an implementation you did not generate.
Spec content is a function of how deeply the spec is consumed, not of how big the project is. I have not seen anyone else make that claim, and I have 2 live projects sitting on either side of it.
The half of a SaaS app the 6 sections never touched
The biggest thing the 6 sections do not reach took a research pass rather than a project to surface.
A SaaS product is 2 planes. The application plane is the product itself: domain entities, workflows, screens. The control plane is tenant, identity, onboarding, provisioning, billing, metering, entitlements and admin. AWS is explicit in its SaaS Architecture Fundamentals that the control plane is itself single-tenant and global across tenants. The Azure multitenancy guidance adds that control planes nest, global to stamp to tenant-level, and that the surface a customer admin self-serves is a distinct thing from your own.
My 6 sections model the application plane. All of it, and only it. Actors, entities, journeys, rules, constraints and open questions describe a logistics portal or a mail client well. They carry no representation of tenancy, plans, entitlements, metering or onboarding, which is the half that makes something SaaS rather than software.
The evidence that convinced me was in my own file. The mail app model grew a capabilities node that was never in the schema. It reached 105 instances, with {off, grace, degraded, on} states, flat entitlement and multi-domain tenancy sitting in a cross-cutting kernel. The concern did not go missing. It arrived unnamed, unschematised and project-specific, which is what an under-specified schema looks like from the inside.
Different apps break in different places
Split the question and you get opposite answers.
The method generalises. BRD to typed artifact to derivation to drift check to consensus gate to open-question ledger. Nothing in that chain is domain-specific, and the field keeps converging on the same properties independently: Spec Kit has /analyze and /converge for drift, ISO 42010 had correspondence rules decades ago, and Volere has had a waiting room for undecided requirements since the 1990s. 3 practitioner communities, 40 years apart, reaching for the same constructs.
The schema does not generalise. The 6 sections are shaped for record-plus-workflow B2B systems, which is not an accident, because the booking system is precisely that. The mail app is record plus communications, close enough that adding mutations closed most of the gap. Move further out and coverage falls off a cliff.
| Archetype | What the 6 sections cannot hold |
|---|---|
| Collaboration / real-time | Concurrency model, conflict semantics, anchor rebinding when a concurrent edit deletes the anchored range, presence, per-user local undo |
| Data / analytics | Semantic layer, join-path resolution, row-level-security predicates, cache keys that include the security context (leave it out and you have a cross-tenant leak) |
| Marketplace / two-sided | Charge topology and the liability cascade it triggers, 3-party disputes, reputation integrity |
| Ledger / transactional | Immutability with correction by reversal, the pending and posted balance pair, reconciliation against external statements |
| AI / agentic | Evaluation as an acceptance criterion, the (prompt, model, tool schema) deployable triple, per-tenant cost metering |
| Regulated | Audit of reads rather than writes, legal hold against erasure requests, break-glass access, scope minimisation |
| Embedded / white-label | Tenant hierarchies deeper than 2 levels, third-party browser context constraints, theming as a bounded contract |
Roughly, and this is a judgment rather than a measurement: application plane in the archetype I know well, about 80%. Application plane elsewhere, 40 to 60%. Control plane, about 10%, and only whatever leaked into that capabilities node. Integration contracts with systems I do not own, near zero, because actors covers humans and nothing covers the systems you must talk to on terms you cannot unilaterally change.
There is a fast way to test whether a spec is complete for its domain. One question each.
| Archetype | The question |
|---|---|
| Records and workflow | Who can change the schema, and what breaks when they do? |
| Collaboration | What happens when 2 users do the conflicting thing at the same instant? |
| Analytics | Whose metric definition wins, and whose rows can this user see? |
| Marketplace | When money goes wrong, whose balance is debited? |
| API-first | What change breaks a customer’s deployed code, and how do you tell them? |
| Regulated | What do you hand the auditor? |
| Embedded | What does the end user see when it fails, and whose brand is on it? |
| AI / agentic | What is the eval threshold, and what can the agent do without asking? |
| Ledger | What invariant holds at every instant, and how do you fix a wrong entry? |
| Scheduling | What happens to the recurring event when timezone rules change? |
| Inventory | What prevents 2 customers buying the last unit? |
| Communications | What stops you sending to someone who opted out? |
Ask the one for your domain. If the spec cannot answer it, the answer gets invented at generation time, and you inherit it.
Getting the AI to write the right shape
The obvious move at this point is to add 12 more fields, and it is the wrong one. Böckeler’s warning is pointed: the review burden of verbose spec artifacts already exceeds the review burden of a code diff, and agents demonstrably ignore spec content despite elaborate checklists. A 20-field model is how this becomes model-driven development with extra steps, and MDD already failed once.
What I do instead is a kernel plus profiles. Small stable core, variable overlay selected per project.
The kernel is the typed 6, with journeys split (it is the weakest of the 6 and it flattens 5 things every source framework keeps apart: commands, domain events, read models, state transitions and error branches), plus a control-plane group and a goals section. Goals is the largest single hole in the survey: about 10 of 30 frameworks carry it first-class, and nothing in my 6 answers “why are we building this and how will we know it worked.”
The profile is picked on the archetype plus 3 dials:
- Tenancy depth: single, tenant, sub-tenant, reseller
- Regulatory load: none, contractual, statutory, certified
- Consistency requirement: best-effort, eventual, strong
The dials do more work than they look like they do. A CRM at high regulatory load with strong consistency is a materially different spec from the same CRM at low load, and stating 3 positions is cheaper than maintaining a library of every combination.
In practice this is a generation step, not a document. Before the model writes anything, it answers 4 questions: dominant archetype, secondary archetype, the 3 dial positions, and consumption depth. Those 5 answers select which sections exist and which are required rather than optional. A ledger profile makes invariants a required section and refuses to emit a file where balances have no stated identity. An agentic profile requires eval thresholds and an autonomy boundary. The booking system’s profile turns on exhaustive typing because everything is generated from it. The mail app’s profile turns typing down and rationale up, because the job there is reconciliation.
One design rule that took me a while to accept, and it runs against my instincts. Do not model policy as state. Plan limits, proration rules, rate limits and retention schedules change far more often than the machines they attach to, and a schema that encodes them as states will thrash. I reach for XState and entity lifecycles by default, so my first instinct was an entitlement state machine. Entitlement is a derived machine over subscription state. The plan to feature to quota mapping underneath it is a table.
The composition is where this actually gets hard. Real products are 1 dominant archetype plus 1 or 2 secondary, and the omissions always live in the secondary one. Nobody forgets the concerns of the thing they think they are building.
The seams nobody specifies
Working through all of this surfaced 4 coupling points that are unspecified more or less everywhere, and not because anyone was careless. They belong to no single concern, so no single source owns them.
- Subscription state to entitlement policy. The grace window between a subscription going past due and entitlement actually being revoked. Whether a downgrade lands at the proration instant or at period end.
- Data sitting under a feature the tenant just lost. Azure documents freemium to paid carefully. Paid to free is unspecified almost everywhere.
- Pushed events against pulled truth. Billing pushes a change event, authoritative entitlement requires a pull, and somebody has to state the maximum staleness you will accept between them.
- Control-plane failure. What degrades when onboarding, metering or entitlement is down.
These are the strongest argument for the whole approach. A seam between 2 well-documented machines is exactly what an open-question ledger and a drift check exist to catch, and they are catchable precisely because the model forces you to name the point where 2 things meet. A prose spec lets that seam stay invisible, because prose can describe both machines beautifully and never ask what happens between them.
The evidence base is 2 projects, sitting at 2 different points on the consumption ladder inside the same practice. Small, and unusually well matched. The whole practice of building this way is about 18 months old, so nobody has more than that yet, and the pair is what makes the comparison say anything at all.
Which brings me back to that capabilities node in the mail app model. 105 entries, with off, grace, degraded and on states, flat entitlement and multi-domain tenancy, all of it grown while I was building rather than designed up front. I found it by counting nodes for the research pass. The 6 sections had no place to put any of it, so it built itself a seventh, and the shape it chose was almost exactly the control-plane group the research went on to recommend. The file worked out what it was missing before I did.
The frameworks, and what each one is for
Everything I checked the 6 sections against, grouped by what it is good at. If you are designing your own field list, the ones marked with a star are where I would start reading.
Domain and behaviour modelling. Where 4 of my 6 sections come from.
| Framework | What it is | Why it matters here |
|---|---|---|
| Domain-Driven Design ★ | Evans’ approach to modelling a business domain in code | Entities, aggregates, invariants, bounded contexts, ubiquitous language. The largest single source |
| EventStorming ★ | Brandolini’s workshop method, coloured stickies on a wall | Separates commands, events, read models and hotspots by colour. The clearest argument that “journeys” is doing too much |
| Cockburn use cases | The fully-dressed use case template | Extensions usually outweigh the happy path, which is the case for treating error branches as required |
| Use-Case 2.0 | Jacobson’s slice-based reworking of use cases | Sizing a use case into independently shippable slices |
| Story mapping | Patton’s backbone, activity and task decomposition | The altitude problem: a spec needs to say which zoom level it is written at |
Rules and decisions. For typing the rules section properly.
| Framework | What it is | Why it matters here |
|---|---|---|
| GUIDE / Business Rules Group | The 1995 paper that defined what a business rule actually is | Separates structural assertions from action assertions from derivations |
| SBVR ★ | OMG’s business vocabulary and rules standard | Alethic vs deontic modality. Must-always-hold is a different kind of rule from violable-with-consequence, and one bucket flattens both |
| DMN | OMG’s decision model and notation | Decision logic belongs in tables, not in state machines |
| BMM | OMG’s business motivation model | Almost entirely about goals and means, which is the field I am missing |
Requirements engineering. For typing constraints and attaching measurement.
| Framework | What it is | Why it matters here |
|---|---|---|
| Volere ★ | The Robertsons’ requirements template | The fit criterion, and the waiting room for undecided requirements |
| EARS ★ | Mavin’s requirement syntax, from Rolls-Royce | 5 sentence shapes. A rule that fits none of them has not been decided yet |
| ISO 29148 | Requirements engineering standard | Mandates owner, source, rationale, priority, status and verification method per requirement |
| ISO 25010 | Product quality model | 9 quality characteristics. The taxonomy an untyped constraints bucket throws away |
| ATAM | SEI’s architecture tradeoff analysis | The response measure. A quality requirement without a number is not a requirement |
Acceptance and examples. For making rules testable.
| Framework | What it is | Why it matters here |
|---|---|---|
| Gherkin | Given/When/Then, and the Rule keyword | The most widely adopted way to attach an example to a rule |
| Example Mapping ★ | Wynne’s 25-minute card exercise | The red card for questions nobody can answer in the room. Same instinct as open questions |
| Specification by Example | Adzic on deriving scope from goals | Examples as the specification rather than as documentation of it |
Architecture description. Mostly for the traceability and viewpoint ideas.
| Framework | What it is | Why it matters here |
|---|---|---|
| ISO 42010 ★ | Architecture description standard | Correspondences and correspondence rules, which is drift checking described decades early |
| 4+1 | Kruchten’s view model | The “+1” scenario view that binds the others |
| C4 | Brown’s 4 zoom levels | Altitude done well, and it defers decisions to ADRs rather than absorbing them |
| RM-ODP | Open distributed processing reference model | Separates static from dynamic schema, which is the case for lifecycles as their own section |
| Zachman | The original enterprise architecture matrix | The Why column, and 6 rows of reification. The official site is currently serving a mismatched certificate, so no link |
| ArchiMate | Open Group modelling language | Its motivation extension is the most complete treatment of goals and drivers anywhere. Full spec needs a free account |
Contracts. The layer the current conversation already covers well.
| Framework | What it is | Why it matters here |
|---|---|---|
| JSON Schema | Structural validation for JSON | Shapes, and notably not relationships. $ref means reuse this shape, never A relates to B with cardinality N |
| OpenAPI | HTTP API description | Operations, responses and error shapes as a published contract |
| AsyncAPI | Event-driven API description | Channels and messages, for the half of a system OpenAPI cannot see |
| Pact | Consumer-driven contract testing | Provider states, and verifying a contract against a running implementation |
AI-native tooling. What the current field is actually shipping.
| Tool | What it is | Why it matters here |
|---|---|---|
| Spec Kit ★ | GitHub’s spec-driven development toolkit | constitution.md, [NEEDS CLARIFICATION], and /analyze and /converge for drift. The closest published thing to this approach |
| Kiro | Agentic IDE with specs and steering files | Persistent project conventions as files the agent always reads |
| Böckeler on spec-driven development ★ | Comparison of Kiro, Spec Kit and Tessl | The spec-first, spec-anchored and spec-as-source taxonomy this article leans on, plus the warning against maximalism |
| Tessl | Was spec-centric AI development | Its @describe and test-linked specs were the sharpest take on traceability. It has since repositioned toward agent skills, so read it as prior art rather than as a current option |
SaaS structure. Where the control plane argument comes from.
| Source | What it is | Why it matters here |
|---|---|---|
| AWS SaaS Architecture Fundamentals ★ | AWS whitepaper | The control plane and application plane split, and that the control plane is itself single-tenant |
| AWS SaaS Lens | Well-Architected lens for SaaS | The 10 concern areas: tenant, isolation, partitioning, onboarding, tiers, consumption and the rest |
| Azure multitenancy guidance ★ | Microsoft’s multitenant series | Nested control planes, tenant lifecycle, and the tenant-level admin surface as its own thing |
| OWASP Top 10 for LLM Applications | Security risks for LLM products | Excessive agency and unbounded consumption, which are the agentic archetype’s spec requirements in disguise |
| RFC 5545 | iCalendar, including RRULE | Recurrence as a data type, and why the this/this-and-following/all edit fork is a modelling decision |