There's a claim that surfaces periodically in programming language theory circles: 'a sufficiently detailed specification is indistinguishable from code.' The implication is provocative — that the line between describing what a program should do and actually doing it is thinner than we think. If your specification is precise enough to remove all ambiguity, you've essentially written the program.
This isn't just philosophy. It has practical implications for how we build software, what role specifications play, and why most bugs aren't coding errors — they're specification gaps. Understanding where specifications end and implementations begin changes how you think about testing, types, and correctness.
Where Most Bugs Actually Come From
Ask a developer where bugs come from and they'll usually say 'code.' But study after study of software defects shows something different: most bugs originate in the gap between intent and implementation. The code correctly does what the developer told it to do. The developer told it to do the wrong thing because the specification — formal or informal — was ambiguous, incomplete, or misunderstood.
A classic example: a specification says 'the system should handle concurrent requests.' What does 'handle' mean? Process them in order? Process them simultaneously? Queue them if there are too many? What's 'too many'? What happens to requests that arrive while the queue is full? These questions aren't implementation details — they're specification gaps. And each unanswered question is a potential bug, because the developer will answer it implicitly through their implementation, and their answer might not match what the users or other developers expect.
The Spectrum of Specification Precision
Specifications exist on a spectrum from informal to formal.
- Natural language. 'The login system should be secure and user-friendly.' This is barely a specification — it's a wish. Every word is ambiguous. What counts as 'secure'? What counts as 'user-friendly'? These are judgment calls disguised as requirements.
- Structured natural language. 'When a user enters an incorrect password three times within 10 minutes, lock the account for 30 minutes.' More precise, but still ambiguous. Does 'incorrect password' include empty submissions? Do the three attempts need to be consecutive? Is the 10-minute window rolling or fixed?
- Type signatures.
authenticate(username: string, password: string) -> Result<User, AuthError>. This specifies the function's interface precisely but says nothing about its behavior. You know the shapes going in and out, but not the relationship between them. - Property-based specifications. 'For all valid username-password pairs in the database,
authenticatereturnsOk(user)whereuser.username == username.' This constrains behavior more precisely using logical quantifiers. - Formal specification. A complete mathematical model of the system's behavior — every state, every transition, every invariant. At this level, the specification and the implementation are almost the same thing.
The closer you get to formal specification, the less room there is for ambiguity — and the less room there is for bugs. But each step also requires more effort and more specialized skills. The practical question is: how far along this spectrum should you go for a given project?
Types as Lightweight Specifications
Type systems are the most widely adopted form of specification in everyday programming. They don't describe behavior, but they constrain it. A function that returns Option<User> instead of User specifies that it might not find a user — and the compiler forces every caller to handle that possibility.
// The type signature IS a specification.
// This function takes a user ID and might not find a user.
// The compiler ensures every caller handles the None case.
fn find_user(id: UserId) -> Option<User> { ... }
// Compare with the stringly-typed version:
// fn find_user(id: &str) -> User // What if ID is invalid?
// // What if user not found?
// // Does it panic? Return null?
// // The type tells you nothing.
// Richer types encode more specification:
enum WithdrawError {
InsufficientFunds { available: Money, requested: Money },
AccountFrozen { reason: String, until: DateTime },
DailyLimitExceeded { limit: Money, spent: Money },
}
fn withdraw(account: &Account, amount: Money) -> Result<Transaction, WithdrawError>
// The error type specifies exactly what can go wrong and what
// information is available when it does. This is specification
// through the type system.
Languages like Rust, Haskell, and TypeScript demonstrate how expressive type systems narrow the specification gap without requiring formal methods training. Algebraic data types, generic constraints, and exhaustive pattern matching collectively specify a lot of program behavior at the type level — and the compiler verifies it automatically.
The key insight: every type annotation is a specification that gets checked automatically. Every bit of behavior you can encode in the type system is a class of bugs you'll never have.
Property-Based Testing: Specifying Behavior
Unit tests are example-based specifications: 'given this specific input, expect this specific output.' They're useful but inherently incomplete — you can only test the cases you think of. Property-based testing inverts this: you specify properties that should hold for all inputs, and the testing framework generates random inputs to find violations.
from hypothesis import given, strategies as st
# Unit test: one example
def test_sort_specific():
assert sort([3, 1, 2]) == [1, 2, 3]
# Property-based test: specifies what sort MEANS
@given(st.lists(st.integers()))
def test_sort_properties(lst):
result = sort(lst)
# Property 1: output has same length as input
assert len(result) == len(lst)
# Property 2: output is ordered
for i in range(len(result) - 1):
assert result[i] <= result[i + 1]
# Property 3: output contains the same elements
assert sorted(result) == sorted(lst)
# These three properties together SPECIFY sorting.
# Any function that satisfies all three IS a sort function.
# The property-based test IS the specification.
The three properties in that test don't just test sorting — they define it. Any function that satisfies all three properties is, by definition, a correct sort. This is the 'specification as code' idea in its most practical form: write properties that define correct behavior, then let the testing framework verify that your implementation satisfies them.
TLA+ and Model Checking
For systems where bugs are expensive — distributed systems, financial platforms, critical infrastructure — formal specification tools like TLA+ provide stronger guarantees. TLA+ (Temporal Logic of Actions) lets you specify a system's behavior as a state machine and then model-check it: exhaustively explore every possible execution to verify that invariants hold.
Amazon has used TLA+ to verify algorithms in S3, DynamoDB, and other critical AWS services. They've publicly described finding bugs that would have been virtually impossible to find through testing — bugs that only manifest under specific interleavings of concurrent operations that occur once in millions of executions.
The TLA+ specification for a consensus protocol like Raft is typically a few hundred lines. It describes every legal state transition, every invariant (e.g., 'at most one leader per term'), and every safety property. The model checker then explores billions of possible execution paths, verifying that no path violates any invariant. This is specification that's genuinely close to code — precise enough that you could mechanically translate it to an implementation.
Design by Contract
Bertrand Meyer's Design by Contract (DbC) sits in a practical middle ground. Each function specifies preconditions (what must be true before the function is called), postconditions (what the function guarantees when it returns), and invariants (what must always be true for the object). These contracts are checked at runtime during development and optionally disabled in production for performance.
def transfer(from_account, to_account, amount):
"""Transfer money between accounts.
Preconditions:
amount > 0
from_account.balance >= amount
from_account != to_account
Postconditions:
from_account.balance == old(from_account.balance) - amount
to_account.balance == old(to_account.balance) + amount
from_account.balance + to_account.balance ==
old(from_account.balance) + old(to_account.balance)
"""
# The postconditions above fully specify what this function does.
# The last postcondition (conservation) catches a class of bugs
# (money created or destroyed) that unit tests might miss.
assert amount > 0, "Transfer amount must be positive"
assert from_account.balance >= amount, "Insufficient funds"
assert from_account != to_account, "Cannot transfer to same account"
from_account.balance -= amount
to_account.balance += amount
The conservation postcondition — that the total money before and after the transfer is the same — is the kind of invariant that catches bugs unit tests rarely cover. It specifies a fundamental property of the system (money isn't created or destroyed) that any correct implementation must satisfy.
Practical Recommendations
Full formal specification isn't practical for most software. But you can narrow the specification gap significantly with techniques that fit into a normal development workflow.
- Use the type system aggressively. Encode constraints as types wherever possible. Prefer
NonEmptyListoverListwhen the list shouldn't be empty. Use newtype wrappers to distinguishUserIdfromOrderIdeven though both are strings. Every constraint you encode in types is a constraint the compiler checks for free. - Write property-based tests for core logic. Identify the invariants your system should maintain and express them as property tests. Even a few well-chosen properties catch bugs that thousands of example-based tests miss.
- Specify the boundaries. The most important specifications are at system boundaries: API contracts, database schemas, message formats. Use OpenAPI specs, Protocol Buffers, or JSON Schema to make these machine-checkable.
- Use TLA+ or Alloy for critical concurrent systems. If you're designing a consensus algorithm, a distributed lock, or a financial transaction system, the cost of formal specification is small compared to the cost of subtle correctness bugs in production.
- Write your spec before your tests. If you can't precisely state what correct behavior looks like, you can't verify it. Spending 30 minutes writing a clear specification of what a function should do (all cases, including edge cases) often reveals design issues before you write a single line of code.
The claim that 'a sufficiently detailed spec is code' isn't quite right — it's more accurate to say that a sufficiently detailed spec removes the room for bugs. The gap between specification and implementation is where ambiguity lives, and ambiguity is where bugs breed. Every technique that narrows that gap — types, properties, contracts, formal methods — makes your software more correct. Not because it prevents coding mistakes, but because it prevents the specification mistakes that coding mistakes usually come from.