How to Write a Technical Specification Document (Template and Examples)
A technical specification says what a piece of software will do, what it will not do, and how the team will check that it works. This guide lists the sections a technical specification document needs, walks through five worked examples, from a half-page config change to a data migration, and ends with a template you can copy.
What is a technical specification?
A technical specification (also called a tech spec or technical specification document) is a written description of what a piece of software will do, what it will not do, and how the team will verify that it works. The engineer leading the work writes it before implementation, so that the people who build, test, and operate the feature share one definition of "done".
It is not a product requirements document (PRD). It is not a Jira ticket with a description field, and it is not a Slack thread where someone said "yeah, that sounds right." A PRD argues that a feature is worth building; the technical spec settles exactly what gets built. It works as 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.
Length follows risk. A configuration change might need half a page; a cross-service feature with a data migration might need five. 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 clarifying question. Most specs fall a little short of that bar, but it is the right one to aim for.
One note on the term. Outside software, "technical specification" also means a product spec sheet: dimensions, materials, tolerances, and ratings for a part or a device. This guide is about software specs. If you want the background on why teams write them first, see What Is Spec-First Development?.
What to include in a technical specification document
Seven core sections belong in every spec, from a half-page config change to a multi-team feature. Larger changes add design sections that describe how the system will be built, which data and interfaces change, and how the change is released and watched. Use the second table to decide which of those your change needs; a section that does not apply is better deleted than filled with "N/A".
Core sections (every spec)
| Section | What it answers |
|---|---|
| Feature name | Which change are we talking about? |
| Goal | What outcome does this deliver, and how will we know? |
| Non-goals | What is deliberately out of scope for this version? |
| Acceptance criteria | Which observable behavior proves it works? |
| Edge cases | What happens with empty, duplicate, concurrent, oversized, or unauthorized input? |
| Output / deliverables | Which artifacts exist when the work is done? |
| Dependencies | What must exist first, and who owns it? |
Design sections (add when they apply)
| Section | What it answers | Include when |
|---|---|---|
| Background | What exists today, and why change it? | Reviewers from outside the team need context |
| Proposed design | Which components change, and how do they interact? | More than one component or service changes |
| API / interface changes | Which endpoints, events, or signatures change, with request, response, and error shapes? | Any interface another team or client depends on |
| Data model and migration | Which tables or fields change, and how is existing data moved? | Any schema change or backfill |
| Non-functional requirements | Latency, throughput, availability, limits, and cost, as numbers | Load, cost, or an SLA could be affected |
| Security and privacy | Who can call or see this? What personal data is stored, logged, or exported? | Auth, permissions, or personal data are touched |
| Alternatives considered | Which options were rejected, and why? | Reviewers are likely to ask "why not X?" |
| Test plan | Which test proves each acceptance criterion? | Criteria need more than unit tests to verify |
| Rollout, monitoring, rollback | How is it released, what is watched, and how is it undone? | Any production risk above trivial |
| Open questions | What is undecided, who decides, and by when? | Anything is still open (the list should be empty before build starts) |
If the design sections start to outweigh the rest, move them into a separate design doc and link it from the spec. The spec stays the short document QA tests against, and the architecture discussion has room of its own. The difference between the two documents is covered in technical spec vs design doc below.
How to write the 7 core sections
These seven sections are the minimum for any technical specification. The design sections above are added on top for larger changes; the template includes both. Skip any of the seven and you leave 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 alice@example.com, 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. 'Alice@example.com' and 'alice@example.com' 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 "alice@example.com"
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.
"Alice@Example.com" and "alice@example.com" are treated as duplicates.
Implementation: normalize to lowercase before comparison.
- Leading/trailing whitespace in email is stripped before comparison.
" alice@example.com " matches "alice@example.com".
- 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., "alice+test@example.com").
Resolution: treat as distinct from "alice@example.com".
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 roughly one page of plain text, the kind of document that takes an afternoon to write, not a sprint. That time buys back days of rework, several rounds of "wait, what should happen when...?" questions in code review, and the kind of production incident that comes from silently merging contacts that have active automations running against them.
The CRM spec is a mid-sized feature. The next section shows four shorter specs for other kinds of change.
More technical specification examples
Specs for different kinds of change put their weight in different places. An API spec spends its space on the contract, a migration spec on ordering and rollback, a UI spec on states and limits. The four examples below are short on purpose, and each one is followed by a note on which part a reviewer would have missed.
Example 1: a small configuration change (admin session timeout)
A small change still needs a goal, a non-goal, and testable criteria. It does not need a design section.
# Feature: Admin session idle timeout
## Goal
Sign out admin users after 30 minutes without activity, to close
finding SR-14 from the 2026 access-control review.
## Non-goals
- No change to session length for non-admin users.
- No "remember this device" option.
## Acceptance Criteria
Given an admin user who has sent no request for 30 minutes
When they send the next request
Then the response is 401 with error code session_expired
and the UI redirects to /login?reason=timeout.
Given an admin user who is active at least every 29 minutes
When 8 hours pass
Then the session is still valid (the 12-hour absolute limit is unchanged).
## Edge Cases
- Background polling (GET /api/notifications) does not count as activity.
- Two open tabs share one session; activity in either tab resets the timer.
## Deliverables
- ADMIN_IDLE_TIMEOUT_MINUTES=30 in config, read at startup.
- Idle check in the auth middleware.
## Dependencies
None.
Each criterion maps to one test. The polling rule is the line a reviewer would have asked for: without it, an open dashboard tab keeps the session alive indefinitely.
Example 2: an API endpoint (invoice CSV export)
For an API change, the contract is the spec: request, response, status codes, and limits. "Returns the invoices" is not testable; the response shape is.
# Feature: Invoice CSV export API
## Goal
Let finance teams export a month of invoices as CSV without opening a
support ticket (about 40 export tickets a month today).
## Non-goals
- No PDF or XLSX export.
- No ranges longer than 31 days per request.
- No scheduled or recurring exports.
## API
POST /v1/invoice-exports
Body: { "from": "2026-10-01", "to": "2026-10-31", "status": ["paid", "open"] }
202 { "export_id": "exp_8f2c", "state": "queued" }
400 invalid_range to < from, or range > 31 days
403 forbidden caller lacks the billing:read scope
429 too_many_exports more than 3 queued exports for the account
GET /v1/invoice-exports/{export_id}
200 { "state": "queued" | "running" | "ready" | "failed",
"download_url": "...", "expires_at": "..." } (url only when ready)
404 not_found export belongs to another account
## Acceptance Criteria
Given an account with 12,000 invoices issued in October
When the user exports 2026-10-01 to 2026-10-31
Then the export is ready within 60 seconds and the CSV has
12,000 rows plus a header row, in the column order under Output.
Given an export that is ready
When 24 hours have passed
Then download_url returns 410 and the file is deleted from storage.
## Edge Cases
- Amounts are decimal strings in the invoice currency ("1234.50"),
never floats; the currency column is always present.
- Customer names starting with =, +, - or @ are prefixed with '
so spreadsheets do not evaluate them as formulas.
- No matching invoices: the export is ready with only the header row.
## Output
- CSV columns: invoice_id, issued_at, customer_name, currency,
amount_due, amount_paid, status
- Signed download URL with a 24-hour expiry.
## Dependencies
- Storage bucket with a 24-hour lifecycle rule. Owner: platform team. Available.
The status codes and the 31-day limit are what client developers need to build against. The formula-prefix rule is the line a security reviewer would otherwise add late, after someone opens a CSV with a cell that starts with =.
Example 3: a data migration (normalize phone numbers)
A migration spec is mostly about ordering and reversibility. Name the phases, what is verified between them, and the point after which rollback is no longer a config change.
# Change: Store customer phone numbers in E.164 format
## Goal
Every customer phone number is stored in E.164 format (+14155550123),
so the SMS provider stops rejecting sends (2.1% failed in October).
## Non-goals
- No phone validation at signup (separate spec).
- No deduplication of customers who share a number.
## Data Model
customers.phone existing, free text, kept until phase 4
customers.phone_e164 new, nullable varchar(16)
customers.phone_parse_err new, nullable text
## Phases
1. Expand: add both columns; new writes fill phone and phone_e164.
2. Backfill: parse existing rows in batches of 5,000, using the
customer's billing country as the default region.
3. Switch reads: the SMS sender reads phone_e164 only.
4. Contract: drop customers.phone after 14 days in phase 3
with no reads of the old column in the query log.
## Acceptance Criteria
Given the backfill has finished
When a row has phone_e164 = null and a non-empty phone
Then phone_parse_err says why ("no digits", "ambiguous region")
and the row appears in the weekly data-quality report.
Given phase 3 is live
When the SMS sender sends to a customer
Then it uses phone_e164, skips customers where it is null,
and logs sms_skipped_no_phone for each skip.
## Verification
- Before phase 3: at least 98.5% of non-empty phones parsed.
- Manual spot check of 200 random rows against the original value.
## Rollback
- Phases 1-3: point reads back at customers.phone (config flag).
- Phase 4 is the point of no return and needs sign-off from the data owner.
The phase list is what makes this reviewable. Without it, "migrate phone numbers" tends to ship as a single deploy that rewrites the column in place, with no way back.
Example 4: a UI feature (saved filters on an orders list)
For UI work, the spec covers states and limits rather than pixels. Link the design file for layout; the spec settles what happens when things go wrong.
# Feature: Saved filters on the orders list
## Goal
Support agents reopen their usual order views in one click instead of
re-entering three to five filters each time.
## Non-goals
- No sharing saved views with teammates in v1.
- No saved views on other list pages.
## Acceptance Criteria
Given an agent with filters status = refund_requested and age > 48h applied
When they click "Save view" and enter the name "Old refunds"
Then "Old refunds" appears under Saved views, and clicking it
reapplies both filters, including after the agent signs out and back in.
Given an agent who already has 20 saved views
When they open the Save view dialog
Then the Save button is disabled with the hint "You can save up to 20 views".
## States
- Empty: "No saved views yet" next to the Save view button.
- Load error: the orders table renders without saved views;
the error never blocks the table.
## Edge Cases
- Duplicate name: inline error "A view with this name already exists".
- A saved view references a status value that was removed: the view
opens, the stale value is dropped, and a notice names it.
- Names are 1 to 40 characters after trimming whitespace.
## Design
Figma: Orders / Saved views, frames 12-15.
Most review discussion for UI work happens in the edge cases. The 20-view limit and the stale-filter behavior are product decisions; writing them down keeps them from being decided in code review.
For two more filled examples in template form, a webhook delivery API and an audit-log table, see spec template examples.
Technical specification template (copy and use)
The template has two parts. The core sections above the divider are required for every spec. The design sections below it are optional: keep the ones that apply to your change, using the table above, and delete the rest. The prompts in brackets guide each section; delete them as you replace them with real content, then share the file for async review before implementation begins.
# 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]
[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]
- [Empty input / null value / concurrent write / permission boundary]
Resolution: [Expected behavior]
## Output / Deliverables
1. [Concrete artifact: API endpoint, database migration, module, etc.]
2. [Feature flag name and default value]
## Dependencies
- [Service, team, or system that must exist before implementation]
Owner: [team name]. Status: [available / ETA]
---- Design sections: keep the ones that apply, delete the rest ----
## Background
[What exists today and why it needs to change. Link the PRD or
ticket instead of restating it.]
## Proposed Design
[Components that change and how they interact. One diagram or a
short numbered flow is usually enough.]
## API / Interface Changes
[Endpoint or event, request and response shapes, status and error
codes, backward compatibility for existing clients.]
## Data Model and Migration
[New or changed tables and fields. Migration phases, backfill, and
the point after which rollback is no longer a config change.]
## Non-functional Requirements
[p95 latency, throughput, availability, rate limits, cost. Numbers,
not adjectives.]
## Security and Privacy
[Who can call or see this. Personal data stored, logged, or exported.]
## Alternatives Considered
[Option, and why it was rejected. One or two lines each.]
## Test Plan
[Which test proves each acceptance criterion: unit, integration,
end-to-end, or manual.]
## Rollout Plan
Stage 1: [Limited rollout: internal or percentage-based]
Observation: [Duration]
Success signal: [Metric or absence of failure]
Stop condition: [Threshold that halts the rollout]
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]
## Monitoring
[Metric or log line, alert threshold, and who is paged.]
## Open Questions
[Question, owner, decide-by date. Empty before implementation starts.]
Download: technical-specification-template.md
For a small configuration change, keep the core sections and delete everything below the divider, as in example 1. For API and database work, the API spec and DB spec templates go deeper on contracts and migrations, and the design doc template is there for when the architecture needs a document of its own.
If you would rather not start from a blank file, the Spec Generator builds a first draft of the core sections from a feature name and a short description. It runs in the browser and needs no account; treat its output as a draft to edit, not a finished spec.
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 alice@example.com, 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).
How to write non-goals that actually hold scope
A non-goal is a specific thing this work will not do, even though a reasonable person might expect it to. The second half of that sentence is the test. "We won't rebuild the billing system" adds nothing if nobody expected it. "We won't handle partial refunds, even though the full refund flow looks similar" is a real non-goal, because someone will try to add it two weeks into implementation. A good list answers "should we also do X?" before anyone asks. The CRM example above has six, and each one is a question that would otherwise have come up in code review.
Most non-goals are not invented; they are extracted from conversations that already happened:
- Scope debates at kickoff. If the team spent ten minutes agreeing to do A but not B, B is a non-goal. Write it down verbatim.
- Adjacent flows. Existing behavior the feature touches but does not change: "Login flow is unchanged; this spec does not modify session lifetime."
- Tempting optimizations. The "while we're in there" moments: "We are not fixing the N+1 query in the list view; that is a separate ticket."
- Dependencies you are not taking on. "We are not introducing a message queue; this feature reuses the existing HTTP path."
Three kinds of non-goal
Label each non-goal by why it is out of scope. Each kind needs a different piece of supporting information, and leaving that information out is how the item comes back as a scope argument.
| Kind | Meaning | Must include | Example |
|---|---|---|---|
| Deferred | Real work, probably planned, but not in this release | A forward pointer: ticket, milestone, or version | "Partial refunds: planned for v2 (JIRA-1234). The full refund flow is built so v2 can extend it without restructuring the refund table." |
| Rejected | Considered and decided against | The reason, so nobody re-litigates it mid-sprint | "SMS password reset: our current provider's delivery reliability does not meet the password-reset requirement. Final unless we change provider." |
| Out of domain | Belongs to another system or team | The owning team, so escalation is clear | "Auth token refresh: owned by the identity team. This feature consumes the existing refresh behavior unchanged." |
For deferred and rejected items, also name the revisit trigger: the metric, customer request, release phase, or finished dependency that would move the item back into scope.
Write non-goals with the same precision as the contract they protect. A vague one gets ignored; a precise one can be checked against a diff and a test plan:
Weak: - No webhook changes. Review-ready: - This release does not add a new webhook event. - It does not change the invoice.updated payload. - It does not require consumers to store tax_region. - Partner notification, schema versioning, and replay behavior are tracked in API-217.
Phrasing that gets ignored
- "X might be added later." Soft; invites scope creep.
- "X is not a priority." Priorities change; commitments don't.
- "We'll see how far we get." Not a non-goal, and not a plan.
Phrasing that holds
- "This release does not include X."
- "X is deliberately out of scope. It is tracked at [link]."
- "We considered X and decided against it because Y."
Give every non-goal a refusal criterion
If a caller can still attempt the excluded behavior, the spec needs an acceptance criterion that describes the refusal. "We won't build partial refunds" is not enough on its own: without a criterion, the attempt ends in an unhandled error and a support ticket. With one, it ends in a clear rejection that QA can test:
Given a user attempts a partial refund via the API
When the request is processed
Then the endpoint returns 400 with error code unsupported_refund_type
and a link to the refund documentation.
Refusal paths are easy to skip, especially in AI-generated implementations, so treat them as first-class criteria (see acceptance criteria for AI coding).
How many non-goals, and when they can move
- Three to six for a mid-sized feature is a useful range. Fewer usually means adjacent scope has not been thought through; many more means the list includes things nobody would have asked for, or the feature is too big. For each item, ask "would anyone actually expect this?" If not, delete it.
- Moving a non-goal into scope needs re-approval from the reviewers who signed off the spec, an updated spec (non-goal removed, acceptance criteria added) before work on the new scope starts, and an explicit look at the timeline. Promoting a non-goal without moving the date is how deadlines slip.
- Add a meta non-goal near the top of the spec, so it does not grow into a design doc:
Meta non-goal: This spec does not describe implementation. Architecture and data structures are engineering decisions. Changing them does not require a spec change unless observable behavior changes.
PRD vs technical spec: what's the difference?
A product requirements document (PRD) and a technical spec answer different questions. The PRD answers "should we build this?" for the people deciding priority. The technical spec answers "given that we are building it, what exactly does it do?" for the people writing the code, the tests, and the rollout plan. Teams that merge the two into one document usually get something that does neither job: executives scroll past API contracts, engineers scroll past business rationale to find the acceptance criteria, and the product framing and the technical detail go stale at different rates, so nobody trusts the whole thing.
| PRD | Technical spec | |
|---|---|---|
| Question it answers | Should we build this, and how important is it? | What exactly does it do, and how will we know it works? |
| Usually written by | Product manager | Lead engineer or a senior engineer on the team |
| Main readers | Product, engineering leadership, design, sometimes executives | Engineering, QA, operations |
| Decision it supports | Go / no-go and priority | What code to write and which tests to run |
| Contents | Customer problem, evidence the problem is real, conceptual solution, expected impact, rough scope and timeline | Goal, non-goals, acceptance criteria, edge cases and failure modes, API and data model changes, non-functional requirements with numbers, rollout and rollback |
| After launch | Becomes historical context | Becomes reference documentation for operating and changing the feature |
The two documents interlock. The spec links to the PRD for motivation instead of restating it, and the PRD does not prescribe architecture. Here is the same checkout feature at both levels:
| The PRD says | The technical spec turns it into |
|---|---|
| Problem: users abandon checkout at the payment step. | Goal: reduce checkout abandonment at the payment step by 20%. |
| Solution: add saved payment methods. | Saved methods persist across sessions, verified by a DB record with user_id and a tokenized method_id. Tokenization goes through the payment provider (PCI DSS requirement). The default method is pre-selected in the UI. |
| โ | Edge case: a saved method is deleted while checkout is open โ refresh the list and show an inline notice. |
| Impact: 5โ8% lift in checkout completion. | Non-functional: p95 checkout API latency under 400 ms; 99.9% success rate on token save. |
| โ | Rollout behind a feature flag by percentage of users. Rollback is a flag flip; saved tokens stay in the DB, unused. |
The empty cells are the point: edge cases and rollback do not belong in a PRD, and they are exactly what the spec exists to settle.
The one-question test. For any document, ask who makes a decision based on it, and which decision. If product leadership decides whether to prioritize the work, it is a PRD. If engineering decides what code to write and QA decides what to test, it is a technical spec. If nobody decides anything, neither document is doing its job.
When you only need one. Small features do not need both. If the "why" is disputable, write a PRD. If the "what exactly" is disputable, write a spec. A one-line request whose motivation is obvious can go straight to a spec. If you are starting from an existing PRD, the PRD-to-spec steps further down show how to translate it.
Technical spec vs design doc vs functional spec
The three names overlap, and teams use them differently. The split below is the most useful one in practice:
| Document | Answers | Usually owned by |
|---|---|---|
| Functional specification | What the user can do: screens, flows, and business rules, described from the outside | Product manager or business analyst |
| Technical specification | What the system does and how it is verified: behavior, interfaces, data, limits, rollout | The engineer leading the work |
| Design doc (technical design spec) | How it will be built and why this approach: architecture, trade-offs, alternatives | An engineer, reviewed by senior engineers |
On small teams, one document often covers all three, which is why the template above has optional design sections. That works as long as the acceptance criteria stay behavioral. A criterion that names internal classes or queues belongs in the design part, not in the list QA tests against.
Turning a PRD into a technical spec
Most specs start from a PRD. The conversion keeps the user outcomes and removes the ambiguity: directional words such as "improve", "support", and "optimize" become observable outcomes, and implied scope boundaries become explicit non-goals. Write the PRD first and the spec second, not in parallel. If spec review shows that the PRD's scope estimate was wrong, update the PRD and have product re-approve it, rather than quietly absorbing the difference in the spec.
Check that the PRD is ready to convert
Converting an incomplete PRD produces a spec with the right structure and the wrong content: criteria that are testable in form but wrong in substance. Stop and send a short gap list back to the PM if any of these is true:
- No success metric. If the PRD does not say what success looks like, the spec cannot write acceptance criteria that satisfy it.
- Open decisions. "TBD", "to be confirmed", or "depends on design" means whoever writes the spec will make that decision silently, without review.
- Unconfirmed dependencies. "Requires the new auth service" with no confirmed availability or API shape means the spec is written against something that may not exist, or may behave differently.
- No rollout or rollback thinking. If nobody has considered a phased release or a way back, the implementation will make those decisions on its own.
Seven conversion steps
- Extract the top user outcomes: what must improve for users, not for systems. These become the Goal.
- Declare what this release will not solve as non-goals.
- Rewrite every requirement as Given/When/Then acceptance criteria.
- Add API, database, and UI impact notes, and mark backward-compatibility assumptions. The API spec and DB spec templates have fields for these.
- List edge cases: invalid input, duplicates, concurrency, permission paths. The edge case checklist covers the usual categories.
- Map each acceptance criterion to an automated or manual test.
- Define rollout and rollback: flags, monitoring metrics, and failure thresholds.
Before implementation starts, check the result: every criterion is binary pass/fail, no requirement depends on undocumented behavior, every external dependency has an owner and fallback behavior, and risky changes have monitoring and rollback triggers. If anything is still unknown, revise the spec before anyone writes code.
Common PRD phrases, translated into spec language
| PRD phrase | Spec goal | Non-goals | Acceptance criterion |
|---|---|---|---|
| "Users can manage their notification preferences." | Users can turn individual notification types on or off without affecting other account settings. | No bulk reset. No push notifications until mobile v2. | Given a logged-in user, when they turn email_marketing off, then preferences.email_marketing = false is saved and the next marketing campaign does not email them. |
| "Improve search relevance." | Results that match all query terms rank above partial matches, within the existing Elasticsearch index. | No ML ranking model. No synonyms or query expansion. | Given the query "annual report 2024", when results are returned, then documents containing all three terms rank above documents containing only one or two. |
| "Support team-based access control." | Workspace admins can assign users to named teams and restrict resource visibility by team. | No cross-team sharing. No sub-teams or team hierarchy. | Given a viewer on team A, when they request a resource owned by team B, then the response is 403 and contains no resource data. |
Who owns what in the handoff
| Role | Owns | Reviews and signs off |
|---|---|---|
| Product manager | The PRD, the user problem, the success metric | The spec's goal and non-goals, for alignment with user intent |
| Tech lead or engineer | The spec: Given/When/Then criteria, technical edge cases, dependency and contract impact | The PRD, for buildability |
| QA | Testability | That every criterion can be verified in the current test environment, and that error and permission paths are covered |
The spec is ready for implementation when all three have signed off. The handoff tends to break in two ways: the PM writes the spec to save time, and it arrives as prose with no acceptance criteria; or an engineer writes the PRD because the PM is busy, and it describes a feature without a user problem. Keep a short record of who decided what at the top of the spec, so later reviewers can tell product decisions from engineering constraints:
Source PRD: Import contacts from CSV Spec owner: Growth engineering Open questions: none Product decisions: - v1 import limit is 1,000 rows. - Duplicate emails inside one file return row-level errors, not a failed import. Engineering decisions: - Imports run asynchronously after upload validation. - Existing contacts are not overwritten in v1. QA evidence: - Fixture: valid_1000_rows.csv - Fixture: duplicate_email_rows.csv - Manual check: progress state and failed-row download
From then on, scope changes go into the spec itself, not into a chat thread or meeting notes. If the PRD later gains a goal, decide whether it belongs to this release: if it does, add acceptance criteria and tests; if not, record it as a non-goal for the next one.
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 whenever 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 |
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 five of the most common.
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 the design section or a design doc, not in the acceptance criteria.
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, using the three kinds described above.
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, and move long architecture discussion into a linked design doc.
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 one of the most common causes 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.
Technical specification FAQ
How long should a technical specification be?
One to three pages of plain text for a typical feature. A configuration change can fit on half a page; a cross-service change with a data migration may need five. If a spec grows past that, it usually covers more than one deliverable and should be split, or its architecture discussion should move into a linked design doc.
Who writes the technical specification?
The engineer who leads the work writes it. Product reviews the goal and non-goals for alignment with the user problem, and QA reviews the acceptance criteria for testability. The spec is ready for implementation when all three have signed off.
What is the difference between a technical specification and a functional specification?
A functional specification describes what the user can do: screens, flows, and business rules, seen from the outside. A technical specification describes what the system does and how that is verified: behavior, interfaces, data changes, limits, and rollout. Small teams often combine them in one document.
Is a technical specification the same as a design doc?
Not quite. The technical spec defines behavior and how to verify it; a design doc explains how the system will be built and why that approach was chosen. Many teams keep both in one file, with the design sections optional, and split them when the architecture discussion gets long.
Can I use this technical specification template in Google Docs or Word?
Yes. In Google Docs, turn on Markdown under Tools, Preferences, then use Edit, Paste from Markdown so the headings carry over. Word has no Markdown paste; convert the file with Pandoc (pandoc spec.md -o spec.docx) or paste it as plain text and apply heading styles.
When should a technical specification be written?
After the problem and priority are agreed, usually in a PRD or ticket, and before implementation starts. Review it asynchronously, resolve the open questions, and keep it current: scope changes during the build go into the spec, not into a chat thread.
References
Standards on requirements and specifications, for readers who want the formal background:
- ISO/IEC/IEEE 29148: the international standard for requirements engineering.
- Software Requirements Specification: the SRS document type this template distills.
Fill a form, get a complete feature spec in Markdown โ free, no signup.