The abstraction staircase in software is old. Compilers took over the what-to-machine-code translation. SQL took over the what-to-query-execution translation. ORMs took over the what-to-SQL translation. Martin Fowler’s January 2026 conversation with Unmesh Joshi and Rebecca Parsons positions LLMs as the next step: natural language takes over the what-to-code translation. The pattern is real, but the conversation surfaces something the pattern comparison undersells.
Every step up that staircase required developers to become more precise about what they wanted. Fortran programmers had to learn to think in loops and conditionals rather than registers. SQL programmers had to learn to think in sets rather than iteration. ORM users had to learn to think in domain models rather than tables. The “how” moved down; the precision requirements for the “what” moved up.
LLMs push that precision requirement to a place most developers have never had to operate. Writing code is a precision discipline: a function either compiles or it doesn’t, behaves correctly or it doesn’t. Writing natural language specifications for LLMs requires a different kind of precision, one that is harder to verify before execution and easier to get subtly wrong.
What “What-Knowledge” Actually Consists Of
When you write code, you hold two kinds of knowledge simultaneously: what the code should do (the intent) and how it should do it (the implementation). These are interleaved in practice. Choosing a data structure, writing a loop, pulling a computation into a helper function, each of these is both a how decision and a what decision. The code is the record of both simultaneously.
When an LLM writes the code, you supply the what without the benefit of the how as a forcing function. The difficulty is specific. Consider the difference between these two specifications for a Discord bot’s rate limiter:
// Specification A (vague what)
"Add rate limiting to the command handler"
// Specification B (precise what)
"Add per-user rate limiting to the command handler: each user can invoke at most
5 commands per 60-second window. Use a sliding window, not a fixed bucket. Store
the window state in Redis with a TTL that matches the window duration. Return a
429-style response with a Retry-After header when the limit is exceeded."
Specification A generates something that probably works but may use a fixed bucket window, an in-memory store that won’t survive restarts, or a response format that doesn’t match the bot’s existing error handling patterns. Specification B generates something that matches the actual requirements.
The difference between A and B is not a vocabulary problem. Writing Specification B requires you to have thought through every decision that the implementation needs to make: that sliding windows and fixed buckets are distinct with different behaviors at bucket boundaries, that in-memory state won’t survive bot restarts, that Redis TTL management for sliding windows has specific implications, what your error response format is. This is how-knowledge converted into what-knowledge. You need to understand the “how” layer well enough to specify the “what” precisely enough that the LLM can realize it correctly. The abstraction does not eliminate the underlying knowledge; it relocates where that knowledge needs to surface.
Unmesh Joshi’s related piece on the learning loop identifies exactly this problem: developers who rely on LLMs before they have internalized the “how” layer cannot write precise enough “what” specifications to get correct results, and they also cannot evaluate whether the generated “how” is correct. The loop requires understanding to enter it.
The Ambiguity Problem
Natural language is ambiguous in ways that code is not. Every developer has had the experience of writing code that was technically correct but didn’t satisfy the actual requirement, because the requirement admitted multiple valid interpretations. LLM-assisted development makes this problem systematic rather than occasional.
Consider a message deduplication system for a bot that needs to handle reconnects. A specification like “don’t process duplicate messages” leaves the following undefined: what makes two messages duplicates (ID, content hash, or both); what the deduplication window is (per-session, per-hour, per-day); what happens when a duplicate is detected (silent drop or logged); where deduplication state is stored (memory, Redis, database); what happens to state on restart.
Each of these is a design decision. In hand-written code, the developer makes every decision explicitly by writing the implementation. With LLM generation, each undecided question gets answered by the LLM based on what seems reasonable given the context it has. Some answers will fit the use case; some won’t, and you won’t know which until the edge case surfaces in production.
The canonical tooling for making “what” specifications precise enough to close this ambiguity is existing software engineering practice: interfaces, types, tests, contracts. Rebecca Parsons’ background in formal methods is relevant here. Pre-conditions and post-conditions are the formal machinery for stating exactly what a function must receive and must return. They are a precise “what” in machine-readable form.
interface MessageDeduplicator {
// Pre: messageId is a non-empty string
// Post: returns true if this messageId has not been seen
// within the last windowSeconds; false otherwise.
// Side effect: records this messageId as seen.
isNew(messageId: string): Promise<boolean>;
// Pre: windowSeconds > 0
// Post: all records older than windowSeconds are removed
cleanup(windowSeconds: number): Promise<void>;
}
This interface tells the LLM enough to generate a correct implementation. It also tells future maintainers what the component is supposed to do, independent of how any specific implementation realizes it. The interface is a what specification that survives implementation changes.
Specification as a First-Class Practice
The practical implication of this shift is that specification writing needs to become a first-class development activity rather than a precursor to real development. In traditional practice, developers often used implementation as a way of working out the specification. You write the code, discover the edge cases, and the final code is both the specification and the implementation simultaneously.
With LLM-assisted development, that process has to move earlier. You work out the edge cases before generating the implementation. The implementation becomes an output to validate rather than a process to work through. This is structurally similar to test-driven development, where writing the test first forces you to specify behavior before implementing it. Kent Beck’s original framing of TDD in Test Driven Development: By Example treated tests as executable specifications; LLM-driven development extends the same logic to natural language and interface-level specs.
The teams getting consistent results from LLM code generation are the ones who have internalized something like this process: specify first, generate second, validate third, iterate on the specification rather than the code. The specification step is not a lightweight prompt; it involves the same kind of thinking that used to go into writing the code directly.
This shift rewards a different part of the developer skill set. Writing a precise specification requires domain understanding, architectural judgment, and the ability to enumerate edge cases. Writing code requires those skills plus syntax and language mechanics. When the syntax is handled by an LLM, what remains is the conceptual work, which most developers have always considered the harder part but which tended to be obscured by the syntactic work surrounding it.
The SQL Parallel
The closest historical analogue to what LLM-assisted specification requires is SQL. A SQL query is both a specification and an implementation. You cannot write it vaguely. Every predicate, every join, every ORDER BY clause is an explicit decision. SQL programmers had to develop a precision discipline around stating what they wanted, because the query planner would faithfully execute whatever they wrote.
-- Vague intent: "get the recent active users"
-- Precise specification:
SELECT id, name, last_seen
FROM users
WHERE status = 'active'
AND last_seen > NOW() - INTERVAL '30 days'
ORDER BY last_seen DESC
LIMIT 50;
The discipline SQL programmers developed, thinking in terms of sets, being explicit about predicates and ordering, understanding how the query would execute, produced better queries because it produced more precise specifications. That same discipline applied to natural language LLM prompts produces better generated code for the same reason.
The harness engineering framing on the Fowler site points at the infrastructure side of this: small focused modules with descriptive names, strong types, and standard interface patterns all compress “what” information into forms the LLM can access without inferring it from implementation details. Precision at the interface level makes LLM generation more reliable not by improving the LLM but by giving it a clearer specification to work against.
Looking Back at the Conversation
The Fowler conversation frames the what/how loop as a mechanism for managing cognitive load: the developer specifies intent, the LLM realizes it, and cognitive capacity is freed for higher-level reasoning. That framing is accurate as far as it goes. The part the conversation surfaces without fully naming is that the “what” side of the loop requires every bit as much rigor as the “how” side used to.
The abstraction ratchet clicked up, and the precision requirements moved with it. You no longer need to hold the syntax of a rate limiter in your head. You need to hold the edge cases of sliding windows, the failure modes of distributed state, and the consistency guarantees your system requires. Different knowledge, comparable depth.
The cognitive load doesn’t disappear; it redistributes. What changes is which kind of expertise you need to bring to the work, and at which point in the development cycle you need to bring it.