How to Write a Software Spec: A Practical Guide for Builders
A practical guide to writing a software spec that actually works — covering structure, precision, data types, edge cases, and what to leave out.
The Problem With Most Software Specs
Most software specs fail before a single line of code is written. Not because developers ignore them, but because the spec itself is too vague to act on.
“Users can log in” is not a spec. Neither is “the dashboard should show relevant data.” These read like requirements, but they don’t give an agent — human or AI — enough information to build anything reliable.
A good software spec is precise. It says what the system does, what it accepts, what it rejects, and what happens at the edges. If someone (or something) can read your spec and build the exact app you imagined without asking a single clarifying question, you’ve written a good spec.
This guide covers how to do that. We’ll go through structure, precision, data types, edge cases, and what to leave out — with concrete examples throughout.
What a Software Spec Actually Is
A software spec is a document that defines the behavior of a system before that system is built. It’s the source of truth for what gets built, not a post-hoc explanation of what was built.
The word “spec” gets used loosely. Some people mean a product requirements document (PRD). Others mean a technical design document. Others mean a user story backlog. For the purposes of this guide, a spec is the thing that tells a builder — human developer or AI coding agent — exactly what to build.
A good spec answers three questions:
- What does the system do? — Features, flows, behaviors.
- What does the system accept? — Input formats, validation rules, constraints.
- What does the system do when things go wrong? — Error states, edge cases, fallback behavior.
If your spec answers all three of these questions with enough precision that a builder could execute without asking clarifying questions, you’re in good shape.
Why Specs Matter More Now
Specs have always mattered. But the stakes changed when AI coding agents entered the picture.
When a human developer reads a vague spec, they do something interesting: they use judgment. They make reasonable assumptions. They ask questions. Sometimes that works out. Sometimes it doesn’t, and you spend two weeks refactoring.
AI agents don’t use judgment the same way. They fill ambiguity with plausible behavior — which may or may not match what you wanted. The more vague your spec, the more the agent improvises. And the more it improvises, the more you end up with code that technically does something but not the thing you intended.
This is why specification precision has become one of the most valuable skills in software development. Vague prose produces vague output. Precise prose produces predictable, verifiable output.
This also distinguishes a real spec from vibe coding. Vibe coding is “throw a prompt and see what comes back.” A spec is a structured, versioned document that stays in sync with your application as it grows.
The Structure of a Good Spec
There’s no single correct format, but a good spec tends to have the same major sections regardless of what you’re building. Here’s a structure that works well in practice.
1. Overview
One or two paragraphs describing what the application does and who it’s for. This is the map before the terrain.
Don’t write marketing copy here. Write a clear, plain description of the system. For example:
“A task management application for small teams. Users can create projects, add tasks with due dates and assignees, and track progress via a Kanban-style board. The app supports multiple workspaces, with team members invited via email.”
Short. Functional. No superlatives.
2. Data Model
This is where most specs fall short. Your data model defines the entities in your system, the fields they contain, and the rules those fields follow.
For every entity, define:
- Field name
- Data type (string, integer, boolean, timestamp, enum, etc.)
- Required vs. optional
- Validation rules (max length, allowed values, format)
- Relationships (foreign keys, cardinality)
Here’s an example for a User entity:
User
- id: UUID, required, auto-generated
- email: string, required, unique, max 255 chars, must match email format
- display_name: string, required, 2–50 chars
- role: enum [admin, member, viewer], required, default: member
- created_at: timestamp, auto-set on creation
- last_login_at: timestamp, nullable, updated on each login
This is not over-engineering. This is the minimum information needed to build a working system without guessing.
3. Features and Behaviors
Break the application into features. For each feature, describe:
- What triggers it
- What inputs it accepts
- What it does
- What it returns or shows
- What happens on error
The key is to write behaviors, not wishes. “Users can filter tasks” is a wish. “Users can filter tasks by status (open, in-progress, done), by assignee (selecting from team members in the current project), and by due date (a date range picker). Filters are applied immediately on selection. Multiple filters can be active simultaneously and are ANDed together.” — that’s a behavior.
4. Edge Cases and Error States
This section is often omitted entirely. That’s a mistake.
For every feature, think through:
- What happens if the input is missing?
- What happens if the input is invalid?
- What happens if a dependency doesn’t exist? (e.g., the user trying to assign a task to someone who left the project)
- What happens if a network request fails?
- What happens if the user hits a rate limit?
Write these out explicitly. Don’t assume the agent will handle them correctly by default. For example:
“If a user tries to assign a task to someone who is no longer a member of the project, show the error: ‘This person is no longer a member of this project.’ Remove them from the assignee dropdown.”
5. Auth and Permissions
Define who can do what. This is especially important in multi-user systems.
A simple permissions table works well:
| Action | Admin | Member | Viewer |
|---|---|---|---|
| Create project | ✓ | ✗ | ✗ |
| Invite members | ✓ | ✗ | ✗ |
| Create tasks | ✓ | ✓ | ✗ |
| View tasks | ✓ | ✓ | ✓ |
| Delete tasks | ✓ | Own only | ✗ |
Be specific about “own only” or “within their workspace” qualifiers. These are the kinds of constraints that turn into bugs if left ambiguous.
6. UI/UX Notes (Optional but Useful)
You don’t need wireframes. But brief notes about layout and interaction can prevent a lot of back-and-forth.
For example:
“The task detail panel opens as a right-side drawer, not a new page. Closing it returns the user to where they were on the board.”
One sentence can prevent an entire misunderstanding.
How to Write with Precision
Precision is the most important quality a spec can have. It’s also the hardest to develop. Here’s how to build it.
Be Specific About Data Types
Don’t say “the user enters their age.” Say “the user enters their age as an integer between 1 and 120.”
Don’t say “the price field.” Say “the price field stores a decimal value in USD, with two decimal places, minimum 0.01, maximum 99999.99.”
Every field in your system has a type. State it explicitly.
Define Allowed Values for Enums
If a field can only take a finite set of values, list them. Don’t say “the order status can be various states.” Say “the order status is one of: pending, processing, shipped, delivered, cancelled.”
Also define what transitions are allowed. Can a shipped order go back to processing? Probably not. Say so.
Distinguish Required from Optional
Every field should be marked as required or optional. And for optional fields, define the default value.
“If no due date is provided, the task is created without a due date and displays as ‘No due date’ in the UI” is better than “due date is optional.”
Write in the Affirmative for Rules
Don’t write “users shouldn’t be able to see other teams’ data.” Write “users can only access data within their own workspace. API routes must return a 403 if the requesting user does not belong to the workspace referenced in the request.”
Affirmative rules are harder to misinterpret.
Use Concrete Examples
Examples are one of the most underused precision tools in specs. Instead of just describing a behavior, show what it looks like:
“Search matches task titles and descriptions. Matching is case-insensitive. Partial matches are included (e.g., searching ‘rep’ returns tasks containing ‘report’ and ‘prepare’). Matches are highlighted in yellow in the results.”
A single concrete example often communicates more than a paragraph of abstract description.
What to Leave Out of a Spec
A spec should define what the system does and what rules it follows — not how it’s implemented. Leave implementation decisions to the builder unless you have a specific reason to constrain them.
Don’t Spec the Technology (Unless You Have To)
Don’t say “use PostgreSQL with a JSONB column for metadata.” If you don’t have a strong reason for that choice, you’re adding constraint without value. Say what data you need to store and what queries you need to run. Let the builder choose the right tool.
The exception: if you have a real technical constraint (e.g., “must integrate with an existing MySQL database”), state it as a requirement.
Don’t Spec the Visual Design in Prose
Long prose descriptions of visual design are usually wrong in practice. “The button should be slightly rounded with a subtle gradient and shadow” becomes a spec that no one will read or follow correctly.
Either link to an existing design system, provide a reference screenshot, or leave visual design decisions open. If you’re building with an AI agent, you can always iterate on the UI after the functional spec is solid.
Don’t Over-Specify Happy Path at the Expense of Edge Cases
It’s common to write three paragraphs about what happens when everything goes right, then nothing about what happens when something goes wrong. Edge cases are where bugs live. A spec that covers them thoroughly is worth ten that don’t.
Annotations and the Next Level of Spec Precision
If you’re writing specs for AI agents specifically, plain prose often isn’t enough. The gap between what a sentence says and what an agent does with it is where most bugs originate.
This is the insight behind spec-driven development as a formal practice. Instead of treating a spec as a document you hand off, you treat it as the actual source of truth — a structured artifact with two layers: readable prose that describes behavior, and annotations that carry the precision.
The annotations are where you put the things that prose can’t express cleanly: data types, validation rules, allowed enum values, cardinality constraints. Think of it as prose with type signatures embedded.
This kind of structured spec doesn’t just communicate intent to a developer. It gives an AI agent enough information to compile a working application without filling in the blanks with guesses. That’s the difference between a spec and a prompt.
How to Handle Common Spec Challenges
Writing Specs for Features You Haven’t Fully Designed Yet
Don’t wait until you have every feature designed before writing anything. Start with the data model and the core flows. Mark anything uncertain with a [TODO] or [OPEN QUESTION] note.
A partial spec is better than no spec. It forces you to think through what you do know, and it makes the gaps visible.
Writing Specs for Complex Business Logic
Complex business logic — pricing rules, permission hierarchies, scoring algorithms — benefits most from examples. For every rule, include at least two examples: one where the rule applies, one where it doesn’t.
For example, if you’re speccing a discount engine:
“If a user has a promo code AND their cart total exceeds $100, apply a 15% discount to the subtotal. The discount does not apply to shipping.
Example: Cart total = $120, shipping = $10. Discount = $18. Total = $112.
Example: Cart total = $80. No discount applied.”
Speccing Integrations
For third-party integrations, define what data flows in and what flows out. You don’t need to understand the API internally — but you do need to define the inputs and outputs from your application’s perspective.
“When a payment is completed, send the order ID, total amount (in cents), and customer email to the webhook endpoint. Expect a 200 response within 5 seconds. If no response or a non-200 response is received, retry once after 30 seconds. If both attempts fail, mark the order as ‘payment_confirmation_pending’ and log the error.”
Where Remy Fits
Remy takes the spec-as-source-of-truth idea and builds it into the product.
In Remy, your spec is a markdown document with annotated prose — readable descriptions of what the app does, with embedded precision for data types, validation rules, and edge cases. Remy’s agent compiles this spec into a full-stack application: real backend, real SQL database, real auth, real deployment. Not a prototype.
The spec stays in sync with the code. When you update the spec, the code updates. When the spec says a field is an integer with a max value of 100, the compiled code enforces that. You’re not hoping the agent interpreted your prose correctly — the spec is the contract.
This is why spec quality matters so much when working with Remy. The better your spec, the more precisely the agent can execute. A vague spec produces a generic app. A precise spec — with typed data models, explicit edge cases, and clear permissions — produces the specific app you described.
If you’ve been spending time building full-stack apps without writing code, adding this level of precision to your spec is the single highest-leverage thing you can do to improve output quality.
You can try Remy at mindstudio.ai/remy.
Common Spec Mistakes (and How to Fix Them)
Mistake 1: Conflating “what” and “why”
“Users need a way to filter tasks because they have too many of them” — the “because” clause doesn’t belong in a spec. A spec describes behavior, not rationale. The rationale belongs in a PRD or background doc. Keep the spec focused on what the system does.
Mistake 2: Using relative terms without reference points
“Large files should be rejected” — how large is large? “Files over 10MB should be rejected with the error message: ‘This file is too large. Maximum size is 10MB.’” That’s a spec.
Other relative terms to avoid: recent, fast, slow, many, few, often, rarely. Replace them with specific values whenever possible.
Mistake 3: Passive voice hiding agency
“An email is sent when a user signs up” — who sends it? From what address? What does it say? What if it fails?
“When a user completes registration, the system sends a verification email to their registered email address. The email contains a single-use verification link that expires after 24 hours. If the email service returns an error, the account is created but flagged as email_unverified, and the user is prompted to resend the verification on next login.”
Mistake 4: Assuming the reader knows the domain
If your application is in a specialized domain — healthcare, finance, logistics — don’t assume the builder knows domain-specific rules. State them explicitly. “Claims must be submitted within 90 days of the service date” is a domain rule that a general-purpose agent won’t know unless you put it in the spec.
Mistake 5: Speccing only the happy path
The vast majority of bugs come from unspecified behavior — what happens at the edges, on error, or in unexpected states. AI agents can fail in specific and predictable ways when the spec leaves these gaps open. Cover the unhappy paths explicitly.
Iterating on Your Spec
A spec is not a one-time artifact. It should evolve with your application.
When a feature changes, update the spec first, then let the code follow. When you discover a bug, trace it back to the spec — often the bug exists because the spec didn’t specify the correct behavior. Fix the spec, then fix the code.
This discipline pays off especially with AI agents. The spec becomes the audit trail for what the system is supposed to do. When output is wrong, you have a reference to compare against. When you hand the project to a new developer or a new agent session, the spec gives them a complete picture of the system without having to reverse-engineer it from code.
This is also why domain experts are becoming builders in ways they couldn’t before. The spec is the interface. If you can describe your application precisely — which domain experts can — you can build it.
Frequently Asked Questions
What should a software spec include?
A good software spec includes: an overview of what the app does and who it’s for, a data model with field types and validation rules, feature descriptions written as behaviors (not wishes), edge cases and error states for each feature, and an auth/permissions section. The more precisely you can define inputs, outputs, and failure modes, the better the output you’ll get from whoever builds it.
How long should a software spec be?
Length should match complexity. A simple CRUD app might have a solid spec in 2–4 pages. A multi-tenant SaaS with complex permissions might need 15–20. The right length is whatever it takes to answer all the questions a builder would have without having to ask you. Don’t pad it for length, but don’t cut precision to keep it short.
What’s the difference between a spec and a PRD?
A product requirements document (PRD) explains why you’re building something — the problem, the user, the business case. A spec defines what the system does — behaviors, rules, data structures. Both are useful. A PRD is for aligning on strategy. A spec is for building. They can live in the same document if your team prefers, but keep the sections clearly separated.
How do you write a spec for an AI agent vs. a human developer?
The fundamentals are the same, but the tolerance for ambiguity is different. Human developers use judgment to fill in gaps. AI agents fill gaps with plausible defaults that may not match your intent. So for AI agents, you need to be more explicit about edge cases, error handling, and validation rules. Concrete examples become especially valuable because they constrain interpretation in ways that prose alone can’t. The concept of specification precision was developed specifically in response to this challenge.
Can you use a spec as the source of truth for the whole project?
Yes — and this is exactly the model that spec-driven development formalizes. Instead of treating code as the source of truth (with documentation as an afterthought), the spec is the source of truth and code is derived from it. This means changes start in the spec, not in the code. It’s a shift in where you work, and it makes iteration more predictable.
What if I don’t know what I want to build yet?
Start with what you do know. Write the overview and the data model, even if both are incomplete. Use [TODO] markers to flag open questions explicitly. The act of writing a partial spec forces you to identify what you haven’t decided yet, which is exactly what you need to move forward. A partial spec is far more useful than a blank one.
Key Takeaways
- A software spec is a behavioral contract: it defines what the system does, what it accepts, and what it does when things go wrong.
- Precision is the most important quality a spec can have. Vague specs produce unpredictable output.
- The core sections are: overview, data model, features and behaviors, edge cases, and auth/permissions.
- Name data types explicitly, define allowed values for enums, distinguish required from optional, and write rules in the affirmative.
- Leave implementation decisions out of the spec unless you have a specific constraint.
- Specs should evolve with the project. When something changes, update the spec first.
- When building with AI agents, specification quality directly determines output quality.
If you want to see what a precise spec can produce, try Remy — a tool built around exactly this idea, where your spec is the source of truth and the code is compiled from it.