Is a Specification the Same as Code? The Truth About Specs vs. Implementation
I wrote a 20-page specification document. It had user stories, acceptance criteria, edge cases, error handling rules, and state diagrams. I fed it to an AI code generator expecting perfect output. What I got was… mediocre code that still needed significant rework.
The problem wasn’t the AI. The problem was my assumption that specifications are fundamentally different from code. They’re not.
The Assumption That Failed Me
I believed this workflow would work:
Vague Idea → Detailed Specification → AI → Perfect CodeThe assumption: Specifications are simpler than code, so writing a good spec should be easier than writing good code.
Here’s what actually happened when I tried to specify a user authentication system:
Users should be able to log in with email and password.Handle invalid credentials gracefully.Support "remember me" functionality.Implement rate limiting for security.Seems clear enough, right? The AI generated code that:
- Checked email format but allowed obviously fake domains
- Implemented rate limiting per IP (wrong for shared networks)
- Used localStorage for “remember me” (XSS vulnerability)
- Handled invalid credentials by revealing whether email existed (information leak)
Each issue came from ambiguity I didn’t notice. Each fix required adding more detail to the spec. By the time the spec was detailed enough to generate correct code… I had basically written pseudocode.
The Spectrum of Precision
This experience led me to understand that specifications and code exist on a continuum, not as separate categories.
Informal English → Structured English → Pseudocode → DSL → High-level Code → Low-level Code ↑ ↑ Vague spec Sufficient detail = executable specLet me show you what happens at each level when specifying the same feature:
Level 1: Informal Specification
Calculate the average of a list of numbersThis requires human interpretation. What if the list is empty? What about null values? What precision for the result?
Level 2: Structured Specification
Given a list of numbers, calculate the arithmetic mean.- If the list is empty, return null- Skip null and undefined values- Round the result to 2 decimal places- Handle floating point precision issuesBetter, but still ambiguous. What’s “handle floating point precision”? What counts as a number?
Level 3: Pseudocode
function average(numbers): if numbers is empty: return null
valid_numbers = filter(n => typeof n === 'number' and not NaN, numbers)
if valid_numbers is empty: return null
sum = reduce((a, b) => a + b, valid_numbers) return round(sum / length(valid_numbers), 2)Now we’re getting somewhere. This is almost code.
Level 4: Implementation
function average(numbers) { if (!Array.isArray(numbers) || numbers.length === 0) { return null; }
const validNumbers = numbers.filter( n => typeof n === 'number' && !Number.isNaN(n) && Number.isFinite(n) );
if (validNumbers.length === 0) { return null; }
const sum = validNumbers.reduce((a, b) => a + b, 0); return Math.round((sum / validNumbers.length) * 100) / 100;}Notice what happened? The structured specification and the implementation express identical logic. The only difference is syntax and execution target.
Why Code Already Is a Specification
Here’s the insight that changed my thinking: every line of code you write is a specification statement.
int count = 7;This line specifies:
- There exists a variable named “count”
- Its type is integer
- Its initial value is 7
- It should be stored in a memory location accessible to the current scope
The C compiler takes this specification and generates assembly that fulfills it. Your code is a spec. The compiler is an executor.
interface User { id: string; name: string; email: string; lastLogin?: Date;}This interface specifies:
- A User must have a string id
- A User must have a string name
- A User must have a string email
- A User may optionally have a Date lastLogin
TypeScript’s compiler is a specification validator. It checks whether your code fulfills the spec.
The Convergence Point
A sufficiently detailed specification is indistinguishable from code. This isn’t philosophy—it’s pragmatics.
Consider this specification for an order total calculator:
REQUIREMENT: Calculate order total
1. Sum all item prices in the cart2. Apply discount code if valid (10% off, one use per customer)3. Add shipping cost (free for orders over $50)4. Handle tax based on customer location5. Round final amount to 2 decimal places6. Return error for invalid discount codesNow compare to implementation:
function calculateOrderTotal(cart, discountCode, customer) { // 1. Sum all item prices const subtotal = cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
// 2. Apply discount if valid let discount = 0; if (discountCode && isValidCode(discountCode, customer)) { if (!customer.usedCodes.includes(discountCode)) { discount = subtotal * 0.1; } }
// 3. Shipping (free over $50) const shipping = subtotal > 50 ? 0 : cart.shippingRate;
// 4. Tax based on location const taxRate = getTaxRate(customer.location); const taxableAmount = subtotal - discount;
// 5. Calculate and round const total = taxableAmount + shipping + (taxableAmount * taxRate); return Math.round(total * 100) / 100;}The spec and the code express the same logic. The code just has more rigid syntax. If you had to write the spec in enough detail to eliminate all ambiguity, you’d end up writing pseudocode that’s almost as complex as the actual implementation.
Where Complexity Actually Lives
This diagram shows where complexity lives at different specification levels:
+------------------+-------------------+----------------------+| Level | Complexity In | Complexity Out |+------------------+-------------------+----------------------+| Informal Spec | Writer's mind | Reader's mind || Structured Spec | Specification | Interpreter's mind || DSL/Pseudocode | Syntax rules | Parser/Compiler || High-level Code | Language rules | Runtime/Compiler || Assembly | Instruction set | CPU |+------------------+-------------------+----------------------+Moving from left to right doesn’t reduce complexity. It just shifts where that complexity must be handled.
What This Means for AI-Assisted Development
When I use AI to generate code from specifications, the quality of output is directly proportional to specification precision. This became clear through trial and error.
My failed approach:
Add a feature to let users reset their password.The AI made assumptions:
- Email-based reset (what if we use SMS?)
- Token expires in 1 hour (what about security requirements?)
- Single-use tokens (what if user requests multiple resets?)
My working approach:
Add password reset feature:- Send reset link via email to registered address- Token is a random 32-character hex string- Token expires after 30 minutes- Token becomes invalid after use- Limit to 3 reset requests per hour per email- Show success message even if email doesn't exist (security)- Log all reset attempts for auditThe second prompt generated code that worked on the first try. But notice something? The precise specification is already structured like code. I spent the same mental effort thinking through edge cases that I would have spent writing the code directly.
The Pragmatic Framework
After months of experimenting, I developed this framework for when to write specs versus code:
+------------------------+------------------------+| Write Spec | Write Code |+------------------------+------------------------+| High-level requirements| Complex logic || Business rules | Edge cases || API contracts | Error handling || Cross-team interfaces | State management || Compliance documents | Performance-critical || Documentation purposes | One-off scripts |+------------------------+------------------------+Specs excel when:
- Multiple stakeholders need to understand
- Requirements might change during discussion
- You need approval before implementation
- The detail level is intentionally vague
Code excels when:
- You need working software fast
- The logic is intricate
- You’re the primary audience
- Iteration speed matters
The Over-Specification Trap
I fell into this trap repeatedly. Here’s a warning sign:
// If your specification looks like this:
"The function should: 1. Accept a parameter 'users' which is an array of User objects 2. Each User object has properties: id (string), name (string), lastLogin (Date or null) 3. Sort the array by lastLogin in descending order 4. For users with null lastLogin, place them at the end 5. For users with equal lastLogin, sort by id ascending 6. Return a new array (do not mutate the input) 7. Handle the case where input is not an array by throwing TypeError 8. Handle empty array by returning empty array"
// You're not writing a specification anymore.// You're writing code in natural language.// Just write the code.Compare to:
function sortUsers(users) { if (!Array.isArray(users)) { throw new TypeError('Expected array of users'); }
return [...users].sort((a, b) => { // Null logins go last if (!a.lastLogin && !b.lastLogin) return a.id.localeCompare(b.id); if (!a.lastLogin) return 1; if (!b.lastLogin) return -1;
// Sort by lastLogin descending, then id ascending const dateCompare = b.lastLogin.getTime() - a.lastLogin.getTime(); return dateCompare !== 0 ? dateCompare : a.id.localeCompare(b.id); });}The specification took more words to express the same logic. And the code is more precise because the syntax eliminates ambiguity.
Lessons Learned
After my failed experiment with over-specification, here’s what I now believe:
1. Invest in precision, regardless of format
Whether writing specs or code, precision matters equally. Ambiguity anywhere creates bugs everywhere. The mental effort is the same.
2. Choose the right level of abstraction
For complex logic, write code directly. It’s the most precise specification language we have. For high-level requirements, keep specs at a level where controlled ambiguity is acceptable.
3. Code is documentation
Well-written code is its own specification. Invest in readable code rather than maintaining parallel documentation that drifts out of sync.
4. AI prompts are specifications
When using AI to generate code, prompts must be as precise as specifications. Vague requests produce unreliable code.
The Reality Check
I still write specifications. But now I understand their purpose differently:
Specifications are for COMMUNICATION between humans.Code is for EXECUTION by machines.When a specification becomes detailed enough to be executed,it has become code—just in a different syntax.The dream of “natural language programming” isn’t wrong because natural language can’t express logic precisely. It’s wrong because making natural language precise enough requires the same effort as writing code in a formal language.
A sufficiently detailed specification IS code. The medium of expression—English, Python, Rust—doesn’t change the fundamental complexity of the problem. It only changes how that complexity is expressed and who (or what) can execute it.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Reddit Discussion: A Sufficiently Detailed Spec is Code
- 👨💻 Formal Specification Methods
- 👨💻 Domain Specific Languages
- 👨💻 Type Systems as Specifications
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments