LLMs as Catalysts for Software Design: Navigating the Nuances of Upfront Specification and Emergent Design

Modern Large Language Models (LLMs) have ushered in a new era of software development, demonstrating a remarkable capacity to generate substantial amounts of code, and in some instances, entire systems, from high-level natural language descriptions. This capability hinges on a critical assumption: that the intended functionality is precisely articulated, enabling LLMs to translate human intent into concrete coding components. However, a deeper examination reveals two fundamental considerations that shape our understanding of this paradigm: the inherent limitations of upfront specification and the profound reality that design often emerges through the iterative process of implementation.
The Inherent Limits of Upfront Specification
The construction of complex software systems is an intricate undertaking, characterized by a multitude of granular design decisions. These decisions, by their very nature, cannot be fully anticipated or dictated solely by an initial, high-level specification. A specification, in this context, serves best as a foundational hypothesis – a starting point to be refined. The true constraints, the nuanced trade-offs, and the often-unforeseen edge cases are invariably discovered through the practical, hands-on process of implementation. This concept, previously articulated as "Upfront Specification Impossibility," underscores that while specifications are far from worthless, the initial document represents a hypothesis subject to revision, rather than an immutable blueprint.
The natural and most effective response to this iterative discovery process is refinement. This involves a continuous cycle of updating the specification, generating code based on the revised directives, meticulously reviewing the output, and then feeding the insights gained back into the subsequent iteration. This feedback loop proves particularly effective when each cycle yields a manageable, reviewable increment of change.
Design Discovered Through Implementation: A Deeper Dive
The act of reviewing code, especially during the nascent stages of design exploration, is fundamentally different from the process of writing it. When reviewing code generated by an LLM, developers validate its alignment with their intent and scrutinize it for potential pitfalls. However, this review process rarely compels the deep wrestling with design decisions that writing code demands. Conversely, the act of writing code necessitates confronting concrete choices – determining where specific responsibilities should reside, defining the boundaries of exposed interfaces to facilitate future extensibility, and ultimately, making the critical design decisions that allow a system’s true architecture to reveal itself.
The choice of programming language and paradigm profoundly influences the design insights gained. A functional programming approach, for instance, will illuminate different facets of a design compared to an object-oriented methodology, exposing distinct idioms and patterns inherent to each paradigm.
The Evolving Role of LLMs in the Development Lifecycle
So, where do LLMs precisely fit into this intricate tapestry of software development? Their utility can be broadly categorized into two primary roles. Firstly, LLMs serve as invaluable collaborators during the formative stages of design and vocabulary establishment, acting as sophisticated brainstorming partners that facilitate the exploration of the design space and the discovery of optimal abstractions. Once this foundational vocabulary is firmly established, LLMs transition to serving as an exceptionally intuitive natural language interface to that established structure.
Domain Abstractions and the Power of DSLs
A particularly insightful framework for understanding this dynamic is rooted in Domain-Driven Design (DDD). DDD’s core tenet revolves around the construction of a shared conceptual model of the domain within the codebase. This model, often referred to as a Ubiquitous Language, becomes the shared vocabulary for evolving the codebase and for the team’s cognitive and communicative processes. Frequently, building a Domain-Specific Language (DSL) atop this model proves highly effective. A DSL provides a constrained syntax for expressing the domain’s core concepts and operations. Viewed through this lens, much of software development can be seen as the ongoing process of building and refining a domain model. The LLM’s role, in this context, bifurcates depending on whether a robust domain model already exists. This article will primarily focus on the synergistic relationship between Domain-Specific Languages (DSLs) and LLMs.
Why DSLs Excel in Tandem with LLMs
The observation that DSLs perform exceptionally well with LLMs is a widely shared experience within the developer community. Languages such as PlantUML, Mermaid, and Graphviz are domain-specific languages designed for visual modeling. SQL serves as a DSL for database querying, and Kubernetes YAML manifests define cloud infrastructure. These are not general-purpose programming languages; they are deliberately constrained, engineered to articulate a narrow set of concepts within a specific domain. It is therefore unsurprising that LLMs exhibit remarkable proficiency in generating Mermaid diagrams, SQL queries, or Kubernetes manifests from plain English descriptions.
This efficacy stems from DSLs’ ability to render LLMs more reliable. DSLs respond exceptionally well to a limited number of in-context examples. In contrast, a general-purpose language like Java can express the same intent in a myriad of valid ways. A DSL, by its very nature, strips away this ambiguity. Providing the model with a few representative examples is often sufficient to ensure reliable generation of the correct syntax. It is important to note that contemporary LLMs have been extensively trained on diverse datasets, including examples of PlantUML and Java fluent interfaces, meaning they are not starting from a position of complete unfamiliarity. The performance of smaller, more specialized models when tasked with novel DSLs remains a fascinating area of future exploration.
For an LLM operating as an agent within an autonomous generate-and-check loop, rather than a single-shot generation, an additional benefit emerges. DSLs invariably include a deterministic validator – be it a parser, a JSON schema, a type checker, or a compiler. The agent can generate a candidate solution, submit it to the validator, and, if necessary, rectify errors without human intervention. Crucially, the error messages are phrased at the domain level – for instance, "you cannot select an action before choosing a client" – rather than as deeply embedded stack traces within generated code. The tooling associated with a DSL thus serves as an excellent harness for LLM-driven development. This will be vividly illustrated in the Tickloom examples, where the DSL’s grammar is enforced by the host language’s compiler, and the resulting executions are automatically verified.
However, it is imperative to acknowledge that this is not a universally applicable solution. The advantages are most pronounced when the DSL remains sufficiently small and constrained, allowing a few in-context examples to effectively convey its usage. Furthermore, there is a discernible upfront investment in designing and maintaining both the language itself and its underlying semantic model. Consequently, the benefits are most concentrated in well-factored, genuinely constrained DSLs that are supported by robust validation mechanisms.
Case Study: Diagram-Rich PowerPoint Presentations with LLMs
The integration of LLMs into custom tooling development has proven remarkably accessible. For educators, particularly in fields like distributed systems, the frequent need to create presentations rich in diagrams explaining complex operational flows within clusters is a common challenge. While UML Sequence diagrams are valuable, presenting an entire sequence diagram at once during an explanation of message flow can diminish their effectiveness. The need for a tool to present sequence diagrams step-by-step within a PowerPoint presentation became apparent. Leveraging LLMs, a tool was developed that processes a YAML structure describing the presentation, incorporating references to PlantUML diagrams, and subsequently generates a PowerPoint presentation. The PlantUML diagrams are annotated with explicit steps, and the tool generates a distinct slide for each step, significantly streamlining the creation of visually informative presentations.
Consider the prompt: "Generate a PlantUML sequence diagram showing a cluster of three nodes: Athens, Byzantium, and Cyrene. Enclose them within a box to denote the cluster. Actor Alice sends a message titled ‘title’ with the content ‘After Dawn’ to Athens. Athens then sends a message to itself. Include a note to signify the state ‘ After Dawn’. Athens sends a message to Byzantium which fails. Athens then sends a message to Cyrene. Place a note to the right of Cyrene. Finally, Athens checks with isQuorumReached and returns a synchronous ‘Success’ arrow back to Alice. Mark each message with a ‘[step]’ indicator."
This prompt, when processed by an LLM, yields the following PlantUML code with step markers:
@startuml
actor Alice
box "Cluster" #lightblue
participant athens
participant byzantium
participant cyrene
end box
'[step]
Alice -> athens: "title", "After Dawn"
'[step]
athens -> athens: save()
note right of athens
state:
After Dawn
end note
'[step]
athens -[#red]x byzantium: "title", "After Dawn"
'[step]
athens -> cyrene: "title", "After Dawn"
note right of cyrene
state:
After Dawn
end end note
'[step]
athens -> athens: isQuorumReached()
'[step]
athens --> Alice: Success
@enduml
This generated PlantUML code can then be used to construct a series of slides within a PowerPoint presentation. To facilitate this, a compact YAML specification was developed to define the presentation structure and the diagrams to be utilized on each slide. This approach empowers the use of LLMs to create presentations detailing intricate distributed systems concepts without the manual effort of animating slides. A sample prompt for generating a slide YAML specification is remarkably straightforward: "Create a slide YAML referring to the diagram ‘quorum-write’ with a title ‘Quorum Write Example’."
This prompt generates a slide specification YAML such as:
- slide:
"Quorum Write Example"
diagram: "quorum-write"
It is crucial to recognize that even when the prompt specifies the creation of a "slide YAML," the output is not arbitrary YAML. Because the tool responsible for generating the PowerPoint presentation and the YAML specification it understands are provided as context within the prompt, the LLM is able to generate a correctly formatted YAML specification that can be directly consumed by the tool. The complete YAML specification for this example can be accessed in a GitHub repository, offering a comprehensive view of the structure and content.
In this single example, the LLM effectively played two distinct roles. Initially, it acted as a co-designer, assisting in the shaping of the step-marked PlantUML extension and the slide YAML that builds upon existing PlantUML tooling. Subsequently, once this nascent DSL was established, the LLM transitioned into its role as a natural-language interface, translating an English request into a valid and functional specification. This division of labor between design assistance and natural language interface will be revisited later in the discussion.
The Foundation: Building the Semantic Model
The example presented above, while illustrative, represents a relatively straightforward application. The YAML served as a carrier syntax, and its parsed syntax tree was directly processed, effectively utilizing the syntax tree itself as the Semantic Model. This approach, however, inherently couples the syntax to the execution semantics. In more complex domains, particularly those involving distributed systems, more sophisticated semantic models are required to accurately represent the domain’s concepts and the design decisions embedded within the codebase. To illustrate this, we will examine an example based on a specialized framework developed for the rapid construction and testing of distributed systems.
Tickloom: A Semantic Model for Distributed Systems
The implementation of distributed systems, such as quorum-based key-value stores or consensus protocols like Raft and Paxos, presents a formidable challenge. Even with incremental guidance through prompts, specifications, or meticulously crafted skill files, the asynchronous runtimes expose an overwhelming landscape of potential implementation decisions. Threading models, networking patterns, storage coordination, retry mechanisms, and timing semantics remain intricately intertwined within the generated code. The problem extends beyond mere code generation complexity to encompass verification complexity. The resulting state space, arising from all possible interleavings across thread scheduling, network delays, process pauses, and clock skew, becomes so vast that systematically reviewing and validating correctness across all interacting behaviors is virtually impossible. This is precisely why tools like Jepsen tests are instrumental in uncovering bugs even in the most rigorously tested distributed systems.
This is the precise juncture where a semantic model proves invaluable. Tickloom is a lightweight framework engineered for the construction and testing of distributed algorithms. Its abstractions are not generic runtimes; rather, they embody a set of design decisions concerning how a distributed process behaves. Each node operates within a single-threaded "tick loop": every invocation of tick() advances a logical clock by one unit and processes pending work in a predetermined, deterministic order (network, message bus, process, storage). Time is quantified in ticks, not milliseconds. Messages are represented as plain Java records. Coordination among replicas is expressed through a Replica base class that inherently understands concepts like peers, broadcasts, and quorums.
Consequently, threading, timing, and network delivery are no longer open-ended questions to be re-evaluated with each prompt. What remains for the algorithm author is the core protocol logic. A quorum replica, for instance, is simply a collection of message handlers articulated within the framework’s defined vocabulary.
Because the framework supplies this essential vocabulary – encompassing terms like Replica, quorumRequest, countResponseIf, MessageType, and Handler – a prompt can remain focused on the protocol level rather than the underlying plumbing.
Consider the prompt: "Using the Tickloom Replica abstraction, implement a quorum-based key-value store. A client GET operation should collect values from a majority of replicas and return the one with the highest timestamp (last-writer-wins). A write should be applied locally only if its timestamp is newer than the currently stored one."
This high-level description generates code akin to the following:
@Override
protected Map<MessageType, Handler> initialiseHandlers()
return Map.of(
LWWMessageType.CLIENT_SET_REQUEST, this::handleClientSetRequest,
LWWMessageType.CLIENT_GET_REQUEST, this::handleClientGetRequest,
LWWMessageType.INTERNAL_SET_REQUEST, this::handleInternalSetRequest,
LWWMessageType.INTERNAL_GET_REQUEST, this::handleInternalGetRequest,
LWWMessageType.INTERNAL_SET_RESPONSE, this::handleInternalSetResponse,
LWWMessageType.INTERNAL_GET_RESPONSE, this::handleInternalGetResponse
);
private void handleClientGetRequest(Message message)
var req = deserializePayload(message.payload(), ClientGetRequest.class);
var internalReq = new InternalGetRequest(req.key());
this.<InternalGetResponse>quorumRequest(LWWMessageType.INTERNAL_GET_REQUEST, internalReq)
.countResponseIf(r -> true) // any response is fine, just need a majority
.send()
.whenComplete((responses, error) ->
if (error != null)
send(createMessage(message.source(),
message.correlationId(),
new ClientGetResponse(req.key(), null, false),
LWWMessageType.CLIENT_GET_RESPONSE));
return;
byte[] highestValue = null;
long highestTimestamp = -1;
for (InternalGetResponse r : responses.values())
if (r.value() != null && r.timestamp() > highestTimestamp)
highestTimestamp = r.timestamp();
highestValue = r.value();
boolean found = highestValue != null;
send(createMessage(message.source(),
message.correlationId(),
new ClientGetResponse(req.key(), highestValue, found),
LWWMessageType.CLIENT_GET_RESPONSE));
);
(Source: Tickloom GitHub Repository)
The semantic model itself functions as a contextual anchor. The prompt references concepts that are concretely represented as types within the codebase. Consequently, the LLM is not tasked with inventing a threading model or a networking layer; instead, it fills in the protocol logic against a stable, well-understood underlying substrate.
The Power of Abstractions: Even Without a Dedicated DSL
While DSLs represent a powerful end of the spectrum, their development can be resource-intensive. Before embarking on the creation of a custom language, it is beneficial to recognize that a well-designed set of abstractions already embodies a lighter version of the same principle. Just as the framework supplied the vocabulary for the quorum store example, a library’s named types and methods constitute a vocabulary that can ground the LLM’s understanding. Tickloom’s semantic model, in essence, comprises four key seams: Process/Replica for compute and message handling, Network for communication, Storage for persistence, and a logical Clock for time management. This decomposition achieves a significant portion of the desired outcome without requiring any novel syntax.
This is why abstractions, not exclusively DSLs, pair effectively with LLMs. A prompt such as "implement Raft as a Tickloom Replica" significantly narrows the state space for exploration. The existing QuorumReplica can be leveraged in context as a practical, worked example.
Example: Crafting a DSL for Distributed System Scenario Testing
Implementing the core algorithm is one facet; rigorously exercising it is another. The subtle defects endemic to distributed systems often manifest in specific temporal orderings: a write operation that replicates to one node just before a reader’s quorum shifts, a network partition that heals at a critical moment, or two coordinators whose clocks have drifted apart. Manually constructing such scenarios against a test kit involves navigating complex futures and meticulously managed tick() loops. Consider a clock-skew scenario written in this manner:
Cluster cluster = new Cluster()
.withProcessIds(Arrays.asList(ATHENS, BYZANTIUM, CYRENE))
.useSimulatedNetwork()
.build(QuorumReplica::new);
cluster.start();
try
cluster.tickUntil(cluster::areAllNodesInitialized);
cluster.setTimeForProcess(ATHENS, 1000L);
cluster.setTimeForProcess(BYZANTIUM, 2000L);
QuorumReplicaClient alice = cluster.newClientConnectedTo(ALICE, ATHENS, QuorumReplicaClient::new);
QuorumReplicaClient bob = cluster.newClientConnectedTo(BOB, BYZANTIUM, QuorumReplicaClient::new);
QuorumReplicaClient reader = cluster.newClientConnectedTo(READER, ATHENS, QuorumReplicaClient::new);
TickCompletableFuture<SetResponse> bobWrite = bob.set(KEY.getBytes(StandardCharsets.UTF_8), "B".getBytes(StandardCharsets.UTF_8));
cluster.tickUntilComplete(bobWrite);
TickCompletableFuture<SetResponse> aliceWrite = alice.set(KEY.getBytes(StandardCharsets.UTF_8), "A".getBytes(StandardCharsets.UTF_8));
cluster.tickUntilComplete(aliceWrite);
TickCompletableFuture<GetResponse> read = reader.get(KEY.getBytes(StandardCharsets.UTF_8));
cluster.tickUntilComplete(read);
assertEquals("B", new String(read.getResult().value(), StandardCharsets.UTF_8));
finally
cluster.close();
(Source: Tickloom GitHub Repository)
The underlying intent – that "Bob writes through Byzantium, Alice writes through Athens, and a reader sees Bob’s value because Byzantium’s clock was ahead" – is obscured by the procedural mechanics. This code is also challenging to verify; numerous incidental decisions (when to tick, how to encode bytes, which factory overload to invoke) present opportunities for LLMs to introduce subtle errors, and reviewers must meticulously examine each one.
To address this, an internal DSL was developed on top of the semantic model. This DSL’s vocabulary directly mirrors that of the scenario itself: servers, clients, their interconnections, the actions each client performs, and the faults present during those actions. The identical scenario, when expressed using the DSL, transforms into:
Scenario<QuorumReplicaClient> scenario =
QuorumStepBuilder.scenario("LWW lost update via server clock skew")
.servers(ATHENS, BYZANTIUM, CYRENE)
.clients(ALICE, BOB, READER)
.client(ALICE).connectedTo(ATHENS)
.client(BOB).connectedTo(BYZANTIUM)
.given(g -> g.serverTimeAt(ATHENS, 1_000L)
.serverTimeAt(BYZANTIUM, 2_000L))
.steps(s ->
s.client(BOB).writes(KEY, "B").expectSuccess();
s.client(ALICE).writes(KEY, "A").expectSuccess();
s.client(READER).reads(KEY)
.expectResponse(v -> "B".equals(v));
);
(Source: Tickloom GitHub Repository)
The DSL provides a thin, declarative surface that compiles down to a pure intermediate representation: a Scenario composed of Steps. Each step encapsulates an Action (a read or write) and optional ClusterEvents (faults such as partitions and message delays). Faults are also expressed in natural language: partition(BYZANTIUM).from(CYRENE), reconnect(BYZANTIUM), delay(INTERNAL_SET_REQUEST).from(ATHENS).to(BYZANTIUM, CYRENE).byTicks(100). The grammar is enforced by the type system through progressive interfaces – a step cannot be declared before the topology, nor an action before a client is selected. This prevents entire classes of malformed scenarios from even compiling. Because the DSL is an internal DSL built in Java, the host compiler validates the grammar intrinsically, and any malformed generation results in a compile-time error precisely pinpointing the illicit step, rather than an unexpected runtime surprise.
Once the DSL is established, a natural language description of a failure scenario maps almost directly onto it. A prompt such as: "Using the Tickloom scenario DSL, write a scenario reproducing the DDIA §10.6 non-linearizable quorum read. A writer connected to Athens sets the key, then updates it while replication from Athens to the other replicas is delayed. Alice, reading through Byzantium, is forced onto a quorum that includes Athens and sees the new value; Bob, reading later through a quorum of Byzantium and Cyrene, still sees the old value," yields a scenario that remains entirely within the DSL’s constrained vocabulary:
Scenario<QuorumReplicaClient> scenario =
QuorumStepBuilder.scenario("Non-linearizable quorum read")
.servers(ATHENS, BYZANTIUM, CYRENE)
.clients(WRITER, ALICE, BOB)
.client(WRITER).connectedTo(ATHENS)
.client(ALICE).connectedTo(BYZANTIUM)
.client(BOB).connectedTo(BYZANTIUM)
.steps(s ->
// Writer sets the key to VOLD initially and it replicates fully
s.client(WRITER).writes(KEY, VOLD).expectSuccess();
// Writer updates to VNEW, but replication from Athens is delayed
s.client(WRITER).writes(KEY, VNEW)
.whileClusterEvent(delay(QuorumMessageTypes.INTERNAL_SET_REQUEST)
.from(ATHENS).to(BYZANTIUM, CYRENE).byTicks(100))
.expectSuccess();
// Alice reads through Byzantium. Force quorum to include Athens by partitioning Cyrene.
// She will read VNEW from Athens.
s.client(ALICE).reads(KEY)
.whileClusterEvent(partition(BYZANTIUM).from(CYRENE))
.expectResponse(v -> VNEW.equals(v));
// Bob reads later through Byzantium. Force quorum to include Cyrene (and exclude Athens).
// The delayed VNEW replication hasn't arrived at Cyrene, so Bob reads VOLD.
s.client(BOB).reads(KEY)
.whileClusterEvent(reconnect(BYZANTIUM))
.whileClusterEvent(partition(BYZANTIUM).from(ATHENS))
.expectResponse(v -> VOLD.equals(v));
);
ScenarioResult result = scenario.run();
(Source: Tickloom GitHub Repository)
Because the surface area is so limited – and the space of valid code the DSL can generate is considerably smaller than that of valid Java programs – the LLM has minimal room for hallucination. Reviewers can interpret the output as a description of an experiment rather than code requiring meticulous line-by-line auditing. Even if the LLM generates an erroneous output, the internal DSL will flag it as a compilation error, enabling the LLM to rectify its mistakes.
The Two-Phase Approach to Working with LLMs
A discernible pattern emerges from the examples discussed: in each instance, the LLM proved beneficial in two distinct capacities.
The initial phase involves the design of the abstraction or DSL itself. In this stage, the LLM is most effectively treated as a brainstorming partner rather than a direct code generator. As articulated earlier, the design decisions that constitute a semantic model cannot be entirely specified upfront; constraints, trade-offs, and edge cases are uncovered through the implementation process. Therefore, this phase is inherently iterative and feedback-driven: a structure is proposed, tested against a real-world case, areas of awkwardness are identified, and the lessons learned inform the subsequent iteration. The LLM accelerates this loop by sketching alternatives, critiquing designs, and porting ideas across languages. However, the developer remains firmly in control, as these are precisely the decisions that require thorough understanding and ownership. Structures that enhance the usability of a DSL, such as progressive interfaces that render an illegal scenario a compile-time error, or a semantic model decoupled from its builder, are outcomes converged upon through iteration, not through a one-time specification and generation process.
The second phase commences once the abstraction or DSL is in place. At this juncture, the LLM’s role shifts: it transforms into a natural-language interface to the developed construct. The prompts presented throughout this article serve as exemplary illustrations: "implement a quorum store as a Tickloom Replica," "write a scenario reproducing the DDIA §10.6 read," "create a slide YAML for this diagram." In each case, the English description aligns almost directly with the defined vocabulary, and the LLM acts as a dependable generator precisely because the abstraction provides both the contextual grounding for the prompt and the validation harness for the result.
The DSL as the Ultimate Source of Truth
A growing trend involves treating prompts as the primary source of truth. A well-designed DSL fundamentally alters this dynamic. One of the key advantages observed when working with DSLs is that the generated program itself frequently becomes the artifact that humans maintain. Because a DSL is dense, expressive, and largely devoid of incidental boilerplate, it encapsulates the essential intent of the solution in a form that remains comprehensible long after its generation. If an LLM generates a Tickloom failure scenario from a natural language request, the resulting scenario is already articulated in the domain’s vocabulary. Should the scenario require modification in the future, there is no need to recover the original prompt and regenerate everything. The DSL possesses sufficient context for the LLM to understand the intent and work with it effectively. The enduring asset is not the prompt, but the DSL and its underlying semantic model.






