How to Write a Technical Spec — Template, Examples & Free Generator
A technical spec is the most underrated tool in software engineering. This guide walks you through every section, shows a complete real-world example, and gives you a copy-pasteable template — plus a free online generator.
What is a technical specification?
A technical specification is a written document that describes what a piece of software should do, what it should not do, and how you will know it is working correctly. It is not a product requirements document (PRD). It is not a Jira ticket with a description field. It is not a Slack thread where someone said "yeah, that sounds right." A technical spec is a written contract between the people who define a feature, the people who build it, and the people who test it.
The defining characteristic of a good spec is testability. Every statement in the document should lead to an observable outcome that someone — a QA engineer, an automated test, a product manager watching a demo — can verify without asking the author what they meant. If a sentence in your spec cannot be tested, it is an opinion, not a specification.
Specs vary in length. A simple configuration change might need half a page. A cross-service feature with data migrations might need five pages. Length is not the goal. Completeness is. A spec is complete when a competent engineer who has never seen the codebase could read it, implement the feature, and pass every acceptance criterion without asking a single clarifying question. That is a high bar, and most specs don't fully reach it — but it is the right bar to aim for.
The rest of this article will show you exactly what goes into a spec, walk you through a complete real-world example, and give you a template you can copy into your next project. If you want a deeper look at the philosophy behind this approach, see What Is Spec-First Development?.
Why write a spec before coding?
The argument for writing a spec before coding is not about discipline or process maturity. It is about cost. Specifically, the cost of finding a wrong decision at different stages of delivery.
A wrong decision caught in a document costs you an hour. In code review, a day. In QA, a week. In production, considerably more.
That cost curve is not theoretical. It shows up in every sprint where a developer builds something reasonable based on an ambiguous ticket, QA discovers the behavior doesn't match what product expected, and the team spends two to three days unwinding the implementation — not because anyone made a mistake, but because nobody wrote down what "correct" meant before coding started.
Specs also solve a coordination problem that meetings cannot. When three engineers sit in a planning meeting and nod along to the same verbal description, they each leave with a slightly different mental model. Those differences stay invisible until code review, when it becomes clear that one person assumed the feature handles archived records and another assumed it doesn't. A spec puts the decision in writing before the divergence happens.
There is a time investment. A medium-complexity feature takes two to three hours to spec properly. But the question is not "does speccing cost time?" — it clearly does. The question is "where do you want to pay the cost of ambiguity?" The cheapest moment is before anyone writes code. The most expensive moment is after the code is in production and a customer has found the inconsistency for you.
Without a Spec
- Ticket says "add contact deduplication"
- Each engineer fills gaps with their own assumptions
- Edge cases surface in QA or production
- Scope disputes happen mid-implementation
- Rollback plan: "we'll figure it out"
- QA writes test cases from verbal descriptions
- Onboarding new engineers requires tribal knowledge
With a Spec
- Spec defines match rules, merge strategy, conflict handling
- Async review catches gaps before coding starts
- QA derives test cases directly from acceptance criteria
- Non-goals prevent scope drift before it begins
- Rollback plan: feature flag, tested, documented
- Product confirms behavior matches intent in review
- New engineers read the spec instead of interviewing the team
| When caught | Typical cost | Example |
|---|---|---|
| In the spec review | 1-2 hours | "We forgot to define what happens on duplicate email" |
| In code review | 1-2 days | Significant refactor because assumptions diverged |
| In QA | 3-5 days | Test cases don't match implementation; rework + retest |
| In production | 1-3 weeks | Incident response, hotfix, customer comms, postmortem |
The 7 sections every technical spec needs
After writing and reviewing hundreds of specs, I have settled on seven sections that every technical specification should include. You can add more for complex features — rollout plans, monitoring requirements, security considerations — but these seven are the minimum. Skip any one of them and you create a gap that someone will fill with an assumption during implementation.
1. Feature name
A short, descriptive name that anyone on the team can reference without ambiguity. "Contact deduplication by email" is a feature name. "Improve data quality" is not — it's a goal that could mean twenty different things. The feature name should be specific enough that if two people mention it in separate conversations, they are definitely talking about the same thing.
Example: "CSV import — bulk contact creation with dedup"
2. Goal
One to three sentences describing what user problem or business outcome this feature solves. The goal is not a description of what the system will do — it is a statement of why the system should do it. A good goal can be tested against: at the end of implementation, someone should be able to look at the running feature and say "yes, this achieves the goal" or "no, it doesn't."
Example: "When two contact records share the same email address, automatically merge them into a single canonical record to prevent duplicate outreach and reduce data drift across the CRM."
3. Non-goals
Explicitly list what this feature will not do. Non-goals prevent more arguments than any other section of the spec. They are not limitations or apologies — they are conscious scope decisions. Without them, well-intentioned engineers will expand the feature to handle adjacent cases, and the project timeline will drift without anyone noticing until it is too late.
Example: "This spec does not cover deduplication by phone number. It does not build a manual review UI. It does not retroactively deduplicate existing records."
4. Acceptance criteria
A list of specific, testable statements that define correct behavior. Each criterion should follow the Given/When/Then format: a precondition, a trigger, and an expected outcome. Good acceptance criteria remove all need for QA to interpret what "working correctly" means — the answer is written in the spec.
Example: "Given a new contact is created with email [email protected], when a contact with that email already exists, then the new contact is not created and the existing contact's updated_at is refreshed."
5. Edge cases
Edge cases are the inputs, states, or conditions that the main acceptance criteria don't cover but that will absolutely happen in production. Empty fields, concurrent writes, case-sensitivity differences, permission boundaries, timezone edge effects. For each edge case, state the input condition and the expected behavior. Don't leave them for engineers to discover during implementation — that is how bugs get shipped.
Example: "Email comparison is case-insensitive. '[email protected]' and '[email protected]' are treated as duplicates."
6. Output / deliverables
Name the concrete artifacts that this spec produces when implementation is complete. This might be API endpoints, database migrations, configuration changes, UI components, background jobs, or feature flags. The purpose is to give the team a checklist: when all deliverables exist and all acceptance criteria pass, the feature is done.
Example: "Deliverables: dedup_service module, findByEmail() query method, dedup_events audit table, DEDUP_ON_CREATE feature flag, CSV import dedup handler."
7. Dependencies
List every external service, team, or system that must be in place before this feature can be built or deployed. Dependencies that are not named before implementation cause sprint-blocking surprises. Name them early so the team can parallel-track the work or adjust the timeline.
Example: "Requires contacts-service v2.3+ with findByEmail endpoint. Requires automations-service to expose getAutomationsByContactId for conflict detection. Feature flag service must support per-account rollout."
Complete example: CRM contact deduplication
Abstract descriptions of spec sections are useful, but what people actually need is a complete, realistic example they can study and adapt. The following is a full technical specification for CRM contact deduplication — the same feature shown on the Spec Coding homepage. Every section is filled in with production-realistic detail. This is long on purpose: a real spec needs to be thorough enough that an engineer can implement from it without asking clarifying questions.
You can copy this entire block and use it as a starting point for your own specs.
# Feature: Contact Deduplication by Email
## Goal
When two contact records in the CRM share the same email address,
automatically merge them into a single canonical record. This prevents
duplicate outreach, reduces data drift across sales and marketing
systems, and ensures reporting accuracy on unique contacts.
Applies to contacts created via any channel: web form, API, CSV import,
and manual entry.
## Non-goals (this version)
- We are NOT deduplicating by phone number or name — email only.
- We are NOT merging contacts across different account tenants.
- We are NOT handling contacts with no email (excluded from dedup logic).
- We are NOT building a manual review UI for potential duplicates.
- We are NOT retroactively deduplicating existing records at launch.
This spec covers new-contact creation and import only.
- We are NOT sending email notifications to end users when a merge occurs.
## Acceptance Criteria
Given a new contact is created via the web form with email "[email protected]"
When a contact with that email already exists in the same account
Then the new contact is NOT created;
the existing contact's updated_at is refreshed;
fields that are null on the existing record are populated from the new data;
the response returns HTTP 200 with the existing contact's ID.
Given a new contact has a different name but the same email as an existing contact
When deduplication triggers
Then the existing contact's name is NOT overwritten;
only null fields on the existing record are populated from the new data.
Given a CSV import contains two rows with the same email
When the import job processes them in order
Then the first row creates the contact;
the second row is treated as an update to the first;
a dedup_event is logged with both source row numbers.
Given a CSV import contains 50,000 rows with 2,000 duplicate emails
When the import job completes
Then all dedup merges are logged in dedup_events;
the import summary shows created_count and merged_count separately;
total processing time is under 120 seconds.
Given the existing contact has del_flag = 1 (archived)
When a new contact with the same email is created
Then the archived contact is NOT treated as a duplicate;
the new contact is created normally.
Given the new contact has an empty or null email field
When deduplication logic runs
Then deduplication is skipped entirely;
the contact is created with a null email.
## Edge Cases
- Email comparison is case-insensitive.
"[email protected]" and "[email protected]" are treated as duplicates.
Implementation: normalize to lowercase before comparison.
- Leading/trailing whitespace in email is stripped before comparison.
" [email protected] " matches "[email protected]".
- Two contacts have the same email and the same updated_at timestamp
(e.g., from a bulk import where all rows share the same timestamp).
Resolution: keep the record with the lower internal ID (creation order).
- Two contacts with same email are owned by different users (User A, User B).
Resolution: merge into the record with the earlier created_at;
update ownership to the owner of the surviving record;
log ownership change in the audit trail.
- Dedup target has active automations (sequences, workflows) referencing its ID.
Resolution: do NOT merge. Flag for manual review.
Create a dedup_conflict record. Notify the owning user via in-app notification.
- API caller sends X-No-Dedup: true header with contact creation request.
Resolution: create the contact without dedup.
Requires admin API key. Return 403 if used with a standard key.
Purpose: data migrations and testing only.
- Contact email contains a plus-alias (e.g., "[email protected]").
Resolution: treat as distinct from "[email protected]".
Do NOT normalize plus-aliases. This is a deliberate product decision —
some users intentionally use aliases to segment contacts.
## Output / Deliverables
1. dedup_service module — core matching and merge logic
2. contacts-service: findByEmail(email, accountId) query method
3. contacts-service: mergeContacts(sourceId, targetId) method
4. dedup_events database table — audit log of all merges
Columns: id, source_contact_id, target_contact_id, trigger (api|import|form),
account_id, merged_fields, created_at
5. dedup_conflicts database table — records where merge was blocked
Columns: id, source_contact_id, target_contact_id, reason, resolved, created_at
6. DEDUP_ON_CREATE feature flag (per-account, defaults to false)
7. CSV import handler updated to call dedup_service per row
8. Web form handler updated to call dedup_service on submit
9. Admin API endpoint: GET /admin/dedup-conflicts?account_id=X
10. In-app notification template for dedup conflicts
## Dependencies
- contacts-service v2.3+ must expose findByEmail(email, accountId).
Owner: contacts team. ETA: available now.
- automations-service must provide getAutomationsByContactId(contactId)
to check for active automations before merge.
Owner: automations team. ETA: Sprint 14.
- audit-log-service must accept dedup_event writes with the schema above.
Owner: platform team. ETA: available now.
- Feature flag service must support per-account boolean flags.
Owner: platform team. ETA: available now.
- Notification service must support in-app notification delivery.
Owner: notifications team. ETA: available now.
## Rollout Plan
Stage 1: Internal accounts only (5 test accounts), DEDUP_ON_CREATE = true
Observation: 48 hours
Success: zero unintended merges in audit log; zero support tickets
Stage 2: 10% of paying accounts, randomly sampled
Observation: 7 days
Success: dedup_event rate matches expected duplicate rate (<3% of new contacts)
Stop-loss: any data loss report from a customer → halt immediately
Stage 3: 100% of accounts
Condition: no issues in Stage 2 after 7 days
Rollback:
Step 1: Set DEDUP_ON_CREATE = false (immediate, no deploy)
Step 2: If merges occurred, run restore-merged-contacts.py --since <timestamp>
Data repair owner: data-eng on-call
Point of no return: if >10,000 merges occurred, automated restore is not safe;
escalate to data-eng lead for manual review
This spec is approximately one page of plain text. It took about two hours to write. Those two hours will save the team days of rework, several rounds of "wait, what should happen when...?" questions in code review, and at least one production incident that would have come from silently merging contacts that had active automations running against them.
For a deeper walkthrough of this same example with commentary on each decision, see Real Example: Building a CRM Feature with Spec-First.
How to write acceptance criteria (Given/When/Then)
Acceptance criteria are the most important section of any technical spec. They are the bridge between "what the product should do" and "what QA can verify." The Given/When/Then format works because it forces you to state three things explicitly: the precondition (Given), the trigger (When), and the observable result (Then). If any of those three is missing, the criterion is incomplete.
The most common failure mode is writing criteria that sound specific but are actually vague. "The system should handle duplicates correctly" feels like a requirement, but it tells QA nothing about what to test. Below are five examples of bad acceptance criteria paired with their improved versions. The difference is always the same: the good version names concrete inputs, actions, and outputs.
Bad
"Duplicate contacts should be merged properly."
Good
"Given a new contact is created with email [email protected], when a contact with that email already exists in the same account, then the new contact is not created and the existing contact's updated_at timestamp is refreshed."
Bad
"The import should be fast enough for large files."
Good
"Given a CSV import containing 50,000 rows with 2,000 duplicate emails, when the import job completes, then total processing time is under 120 seconds and the import summary displays created_count and merged_count separately."
Bad
"Error handling should be robust."
Good
"Given the contacts-service returns a 503 during a dedup lookup, when the web form handler catches the error, then the new contact is created without dedup and a warning is logged to the dedup_errors table with the original request payload."
Bad
"Users should be notified about conflicts."
Good
"Given a dedup target has active automations referencing its contact ID, when a merge is attempted, then the merge is blocked, a dedup_conflict record is created, and the owning user receives an in-app notification with the conflict details and a link to the admin resolution page."
Bad
"The API should return appropriate status codes."
Good
"Given a standard API key is used with the X-No-Dedup: true header, when the contact creation request is processed, then the API returns HTTP 403 with error code ADMIN_KEY_REQUIRED and the contact is not created."
Notice the pattern. Every good criterion names a specific precondition ("Given a CSV import containing 50,000 rows"), a specific action ("when the import job completes"), and a specific, measurable outcome ("then total processing time is under 120 seconds"). QA can turn each of these into an automated test without asking the author a single question. That is the standard you are aiming for.
One practical tip: write your acceptance criteria before you discuss implementation. If you write them after a technical design conversation, the criteria tend to describe the system's internal behavior rather than its observable outputs. Acceptance criteria should be written from the perspective of someone testing the system from the outside — they should not reference internal method names, database columns, or implementation details unless those details are part of the public contract (like an API response body).
Feature spec template — copy and use
Below is a blank template with all seven sections. Copy it into a markdown file, fill in each section, and share it for async review before implementation begins. The template includes prompts in brackets to guide you through each section — delete the prompts as you replace them with real content.
# Feature: [Short, descriptive feature name]
## Goal
[1-3 sentences. What user problem or business outcome does this solve?
Write it so someone can look at the finished feature and say
"yes, this achieves the goal" or "no, it doesn't."]
## Non-goals (this version)
- [What this feature explicitly will NOT do]
- [Scope boundary that prevents drift]
- [Adjacent feature that is out of scope and why]
## Acceptance Criteria
Given [precondition — a specific system state or user context]
When [trigger — a specific action the user or system takes]
Then [outcome — the observable, testable result]
Given [precondition]
When [trigger]
Then [outcome]
Given [precondition]
When [trigger]
Then [outcome]
[Add as many criteria as needed. Each one should be independently
testable by QA without asking the author for clarification.]
## Edge Cases
- [Input condition or system state that the main criteria don't cover]
Resolution: [What the system does in this case]
- [Another edge case]
Resolution: [Expected behavior]
- [Empty input / null value / concurrent write / permission boundary]
Resolution: [Expected behavior]
## Output / Deliverables
1. [Concrete artifact: API endpoint, database migration, module, etc.]
2. [Another deliverable]
3. [Feature flag name and default value]
## Dependencies
- [Service, team, or system that must exist before implementation]
Owner: [team name]. Status: [available / ETA]
- [Another dependency]
Owner: [team name]. Status: [available / ETA]
## Rollout Plan (optional but recommended)
Stage 1: [Limited rollout — internal or percentage-based]
Observation: [Duration]
Success signal: [Metric or absence of failure]
Stage 2: [Broader rollout]
Observation: [Duration]
Success signal: [Metric]
Rollback:
Step 1: [Immediate reversal — e.g., feature flag off]
Step 2: [Data repair if needed]
This template works for features of any size. For a small configuration change, you might fill in three acceptance criteria and skip the rollout plan. For a complex cross-service feature, you might expand each section significantly. The structure stays the same.
If you would rather not start from a blank file, the Spec Generator tool builds a structured spec from a short feature description. You type in the feature name and a few sentences about what it should do, and the tool generates a spec with all seven sections pre-filled with reasonable starting points that you can edit.
5 common mistakes in technical specs
Writing specs is a skill, and like any skill, there are patterns that look right but consistently cause problems. These are the five mistakes I see most often when reviewing specs across teams.
1. Writing acceptance criteria that describe implementation, not behavior
A criterion like "the dedup_service calls findByEmail and merges the records" describes how the system works internally. It is useless to QA because they cannot observe internal method calls from outside the system. Acceptance criteria should describe observable inputs and outputs: what the user sees, what the API returns, what appears in the database after the action completes. Implementation details belong in the code, not the spec.
Fix: Rewrite every criterion from the perspective of someone testing the system through its public interfaces. If the criterion mentions a private method name, internal queue, or implementation pattern, it belongs in a design doc, not a spec.
2. Skipping non-goals entirely
When there is no non-goals section, scope is defined only by what the spec includes. But features are surrounded by adjacent work that reasonable people might assume is in scope. If the spec for contact deduplication doesn't explicitly say "we are not deduplicating by phone number," at least one engineer will ask about it during implementation, and the answer will be an ad-hoc decision in a Slack thread that nobody can reference later. Non-goals are cheap to write and prevent expensive mid-sprint scope arguments.
Fix: Before finalizing any spec, ask: "What is the most likely thing someone will assume is included but isn't?" Write those down as non-goals.
3. Treating edge cases as an afterthought
A spec with four acceptance criteria and zero edge cases is not a complete spec. It covers the happy path and leaves everything else to the engineer's judgment. The edge cases section is where you document what happens with empty inputs, null values, concurrent operations, permission boundaries, and unexpected system states. Every edge case you don't document is a potential production bug that QA won't test for because nobody told them it was relevant.
Fix: Go through each acceptance criterion and ask: "What happens if this input is empty? Null? A duplicate? Extremely large? Submitted simultaneously by two users? Submitted by a user without permission?" Write down the answer for each.
4. Writing specs that are too long to review
A fifteen-page spec with exhaustive technical detail, architecture diagrams, and commentary on alternative approaches is impressive but counterproductive. Nobody will review it thoroughly, which means the review conversation that makes specs valuable never happens. The spec becomes a compliance artifact instead of a communication tool. The goal is a document that a reviewer can read and provide substantive feedback on in thirty minutes or less.
Fix: Aim for one to three pages of plain text for a typical feature. If the spec is growing beyond that, consider whether you are combining multiple features into one spec. Each spec should cover one deliverable unit — something that can be built, tested, and shipped independently. If the feature is genuinely complex, break the spec into multiple documents with clear boundaries.
5. Not naming dependencies until implementation starts
A spec that says "integrate with the notifications service" without confirming that the notifications service supports the required functionality is a spec that will stall during implementation. Dependencies are the single most common cause of sprint-blocking surprises. If a dependency is not confirmed before development starts, you are gambling that it will be available when you need it — and that gamble fails often enough to be worth addressing in the spec.
Fix: For each dependency, name the specific capability needed, the team that owns it, and its current status (available now, available by date X, or not yet built). If a dependency is not yet available, the spec should state what happens to the timeline if it slips. This is not pessimism — it is planning.
Try the free Spec Generator
If you want to skip the blank-page problem entirely, the Spec Generator on this site builds a structured technical specification from a short feature description. You provide the feature name and a few sentences about what the feature should do, and the tool generates a complete spec with all seven sections: goal, non-goals, acceptance criteria in Given/When/Then format, edge cases, deliverables, and dependencies.
The generated spec is a starting point, not a final document. It gives you a solid structure with reasonable defaults that you can edit, expand, and share with your team for review. Most users find that starting from a generated draft is significantly faster than starting from a blank template — especially when writing specs is new to the team.
The tool is free, runs in your browser, and does not require an account.
Field example: the section reviewers actually needed
When a spec feels complete but review still drags, the missing section is often not "architecture." It is the operational decision that tells the team when to stop, rollback, or reject a result.
Weak rollout note: - Release behind a feature flag. Review-ready rollout note: - Enable for internal accounts first. - Move to 10% of workspaces after 24h with error rate within 1.2x baseline. - Stop rollout if duplicate merge jobs exceed 3 per hour. - Roll back by disabling contact_dedupe_v2; no schema rollback required.
This kind of note shortens review because it turns "looks risky" into a checklist the team can accept or reject.
Starter Review Block to Copy
Use this as the smallest practical artifact when a team is trying spec-first on a real change. It is deliberately short so it can live inside a ticket or PR.
Spec-first starter block: How to Write a Technical Spec — Template, Examples & Free Generator Decision to make: - Learn how to write a technical specification with a complete template, real-world example, and free online generator. Owner check: - Product owner: - Engineering owner: - QA or operations reviewer: Scope boundary: - In scope: - Out of scope: - Assumption that still needs approval: Acceptance evidence: - Test or fixture: - Log, metric, or screenshot: - Manual review step: Scope boundary: the reviewer must be able to reject unclear goals, missing non-goals, and criteria with no evidence. Reviewer prompt: - What would still be ambiguous to someone who missed the planning meeting? - What evidence would make this safe enough to ship?
Flagship Use Path
This is one of the primary Spec Coding references for Technical spec drafting. Use it with a real ticket, pull request, or release review instead of treating it as background reading.
- Start here when: a feature is bigger than a ticket but not big enough for a heavy design doc.
- Copy this: the complete spec skeleton and the decision record sections.
- Evidence to attach: owner, dependency, edge case, rollout, and test-evidence fields filled before coding.
- Pair it with: Feature Spec Generator and Feature Spec Template.
Flagship review path: - Open this page during planning or review. - Copy the relevant artifact into the work item. - Replace example values with your system, owner, and failure mode. - Block implementation if the evidence line is still blank.
Editorial Review Note
Reviewed Apr 29, 2026. This update added a reusable artifact, checked the article against the related topic hub, and tightened the next-step links so the page works as a practical reference rather than a standalone essay.
Topic Path
This article belongs to the Spec-First Development track. Start with the hub, then use the checklist, template, or tool below on a real project.
Keep reading
Fill a form, get a complete feature spec in Markdown — free, no signup.
Editorial note
- Author details: Daniel Marsh
- Editorial policy: How we review and update articles
- Corrections: Contact the editor