Article URL: https://martinfowler.com/articles/llm-and-dsls.html Comments URL: https://news.ycombinator.com/item?id=48918575 Points: 58 # Comments: 30

LLMs generate code incredibly fast, but to ensure they generate exactly what is intended, they need clear boundaries. Abstractions and Domain-Specific Languages (DSLs) provide a strong harness that guides LLMs right from the start. The example of Tickloom - a domain model and DSL for illustrating distributed system behavior - shows how we can use an LLM as a partner to iteratively build a DSL and as a natural language interface to use it. Such a DSL can act as the key source of truth for software systems in the world of LLMs. Unmesh is a Distinguished Engineer at Thoughtworks, based in Pune, India. He is the author of Patterns of Distributed Systems. Modern LLMs possess an incredible capability. They can generate large amounts of code, and sometimes entire systems, from just a high-level natural language description. An important assumption here is that the 'intent' of what needs to be built is well articulated, using precise words that LLMs can map to coding building blocks. However, there are two important points worth noting: the limits of upfront specification, and how design is discovered through implementation. Building large systems involves a great many small design decisions, and these cannot all be known in advance or driven entirely from a high-level spec. A specification is at best a starting hypothesis: the real constraints, trade-offs, and edge cases are discovered iteratively, as we proceed with the implementation. We discussed this at length in an earlier article, where we called it Upfront Specification Impossibility. The point is not that specs are worthless, but that the first one is a hypothesis to be revised, never a finished blueprint. The natural response is to iterate: refine the spec, generate code, review what comes back, and feed what we learn into the next round. That loop works well when each round produces a small, reviewable change. Reviewing code, particularly while we are still discovering the design, is not the same as writing it. While reviewing the generated code, we review through the chunks validating if it maps to our intent and looking for possible pitfalls. But reviewing rarely forces us to wrestle with the design decisions. Writing code, by contrast, forces us to think through concrete decisions—such as where a responsibility belongs or what boundaries should be exposed so the design can be extended further. It is in making those decisions that a design most fully reveals itself. Code has two distinct but intertwined purposes. It is a set of instructions for a machine, and it is also a conceptual model of the problem domain. A well designed codebase is a representation of the vocabulary of a domain. These abstractions reveal themselves only as developers build the software. Programming languages act as thinking tools, enabling the construction of a conceptual model that supports later evolution. With LLMs, code acts as essential context: good abstractions, executable behavior, tests, types, and invariants all help constrain the model and make its output more useful. The programming language and paradigm we code in shapes the design insight we get. A functional design approach or an object-oriented design approach reveals different aspects of the design, along with idioms and patterns that are natural to the paradigm.