The What/How Loop: How LLMs Fit Into Real Software Design
Software development is often mischaracterized as a linear process: take requirements, translate them into code, and ship it. This view is misleading for newcomers and managers alike, and it has become more prominent with the rise of LLMs. Phrases like "human in the loop" imply that the machine does the heavy lifting of turning specifications into syntax, with people only stepping in to clean up failures.
Experienced programmers know the real challenge is different. It's not about converting requirements to code in one pass; it's about building systems that survive change. And the key to doing that well, as Martin Fowler notes, is managing cognitive load for the people who will later need to modify the system. A well-structured system lets a developer understand only a few hundred lines to make progress, rather than needing to hold a million in their head.
The Intertwining of Intent and Mechanism
At its core, programming maps the "real" domain—the What—onto a computational model—the How. But this is not a one-way translation. As Unmesh Joshi explains, the two are locked in a continuous feedback loop: refining intent reveals mechanisms, and implementing mechanics reveals the true nature of the system. The question "what is this for?" persists at every level, from a system's user journeys down to a single function's purpose, and the answers shape structure and naming.
Rebecca Parsons emphasizes that a crucial part of the How is choosing a computational model to represent the domain—a state machine, a table, a stream, or a log. This mapping is not neutral. Representing something as a state makes you reason about it differently than if you represent it as a transition. The shape of this mapping often dictates where the solution's structure emerges, and the language paradigm you choose will either ease or hinder that representation.
Fowler adds that domain modeling isn't just about data structures; it's about modeling computation. When the What and How are intertwined, you face circular dependencies when trying to reason from broad abstractions. His preferred escape is to start with concrete scenarios and use them to drive the abstractions. Those scenarios operate at two levels: system-level use cases that define boundaries, and module-level test cases (Given → When → Then) that solidify the contract for a single function.
TDD as a Design Strategy
Test Driven Development (TDD) operationalizes this What/How feedback loop. It works because it makes the loop explicit:
- Locking the What: Writing a failing test first forces you to answer naming, inputs, and outputs before getting distracted by implementation. You act as a client of your own API.
- Iterating the How: With a red test, you have license to implement the simplest thing that makes it pass.
- Refining structure: In the refactoring step, the implementation often reveals that the API design—the What—was awkward or leaky, leading to a better model.
Tests encourage thinking about an interface without coupling it to an implementation. When you write a test, you approach your code as a user, which reveals whether the API is usable. While this won't lead to new model insights with every test, it happens often enough to matter.
This connection between TDD and use cases is important: both focus on the What. Tests should be behavior-focused, not implementation-coupled. Simply passing a test is only a baseline. The primary goal is to build structure that can accommodate future scenarios without breaking current ones, achieved through cohesion—grouping parts that share business intent—and decoupling—separating parts with different reasons to change.
Tools for Shaping Abstractions
Once you have sets of similar behavior, you extract them into modules and sub-functions, capturing variation through parameters. These form domain-specific abstractions that make cognitive load manageable. Programming paradigms provide conventions for these organizational decisions. Object-oriented design bets that actions (interfaces) remain stable while types change; functional programming often treats data shapes as stable while transformations vary.
Fowler cautions against treating these as separate paradigms, preferring to view objects, first-class functions, polymorphism, and pipelines as tools any language can offer. What matters is that these tools allow developers to create their own abstractions—a capability that emerged with high-level languages and has been refined ever since. As abstractions stabilize, programming looks increasingly declarative, expressing intent with well-established patterns like SQL for queries or makefiles for builds.
Communities reinforce this vocabulary. Idioms like "use a List-and-Watch pattern with etcd" or "use message passing with a select loop over a channel in Go" provide shortcuts for communication and implementation decisions. These human patterns give developers a shared language for translating intent into code.
LLMs as a Fluid Translation Layer
LLMs perform best within this mature ecosystem of abstractions and community idioms. When you prompt with a stable vocabulary—"order requests by delivery timestamp in a priority queue"—the generated code tends to follow well-understood patterns, making it predictable and easier to review.
Joshi's experiments illustrate the limits of prompting without structure. Asking an LLM to derive an implementation for a miniature object store produced procedural, hard-to-understand code. Pushing it to "refactor" swung the pendulum the other way, creating too many layers and classes. Working through the problem step by step, with tests driving refinement, yielded cleaner abstractions.
This is why prompts alone don't build solutions. They satisfy a scenario without solidifying architecture for future ones. Tests generated to improve coverage miss the point: the goal isn't passing tests but building maintainable structure. LLMs act best as a translation layer inside a human-driven What/How loop—quick to sketch a first version, but the developer must still shape the structure through writing and refactoring.
Because LLMs operate at lower precision, they encourage fluid exploration. Fowler is interested in whether this fluidity can help developers move more easily toward the precision of an alternative computational model—building a semantic model, as described in the DSL book—without taking away the pleasure of model-building itself.
There's a hidden dependency though: LLMs rely on abundant training data from mature languages. Parsons raises the critical question of where new languages and paradigms will come from when the training data is sparse. Fowler draws a parallel to writing: the best work "defamiliarizes" the familiar, but LLMs output an averaged rehash of the familiar by design. That said, they can still hallucinate unfamiliar ideas—random creations with potential—which humans can then select, combine, and develop into something better. Asking an LLM for multiple alternatives and assembling the best bits may be the most productive use. The archetype remains human judgment, but LLMs gesture toward something novel, which may prove especially useful as the trail goes cold.



