Strategy, Experience, Technology

LLMs and Domain-Specific Languages: Why AI Needs Boundaries

Why vibe coding still needs iteration, and how domain-specific languages make LLM-generated software constrained, testable, and durable.

Mert Barbaros ·

Vibe coding made software feel open to anyone stubborn enough to keep prompting. Describe an outcome, inspect what the agent produces, test it, correct it, and repeat. I have used that process to build and ship iOS products such as Truth & Twist, a collection of brain games, and Let's Talk: Question Cards.

That experience is why I now think LLMs and domain-specific languages belong together. The excitement of creating something from a sentence is real. So is the weight of deciding everything the sentence leaves undefined.

I started for the speed, but what kept me was the pleasure of turning an idea into something real and interactive. Over time, I discovered the other side of that freedom. Every decision left out of the prompt still has to be made by someone. If I do not make it explicitly, the model usually makes it quietly. Creativity is enjoyable. Carrying every undefined decision is not.

Over the last two years, I have worked with Claude Code, Codex, Gemini CLI, and many other AI development tools. Each generation has become more capable. Context windows have expanded. Subagents can investigate focused parts of a larger task in parallel.

Almost everything improved. The number of iterations did not.

The cycle still looks familiar: generate, test, discover an assumption, correct it, and uncover the next assumption. As a project becomes more complex, the model can drift away from the business logic and value proposition. At the same time, gaps appear in coding standards, architecture, testing, privacy, and security.

Better models reduce many mistakes. They do not remove the need for standards. In fact, stronger models can make standards more important because they generate more code, touch more files, and make more decisions before a human stops to inspect the direction.

Natural language is excellent for starting software and terrible at finishing its definitions. A requirement can look obvious in a paragraph, then produce five incompatible implementations because every important noun is still negotiable. An LLM can automate that ambiguity at remarkable speed.

My thesis is simple: The LLM should translate flexible human intent. The domain-specific language should constrain that intent into a small, executable, and verifiable language. The model provides reach. The DSL provides walls.

A domain-specific language, or DSL, is a small language designed for one narrow problem area. SQL is a familiar example. A DSL for a word game might describe valid guesses, scoring, attempts, and win conditions without asking the author to rebuild the game engine for every new mode.

The semantic model defines what the domain concepts mean and how they relate. The validator is the automated checker that rejects instructions that break those definitions.

That pipeline matters for three reasons. It reduces the number of legal answers, makes generated output mechanically checkable, and preserves product intent after the original prompt has disappeared into chat history.

DSL LLM Flow Diagram
DSL LLM Flow Diagram

Read the diagram from left to right. The LLM translates intent, the DSL limits the legal expression, the semantic model gives that expression meaning, and the validator either permits execution or returns a domain-level error for repair.

Core principle: The LLM handles translation. The DSL defines the legal language. The validator enforces it.

These sources form a coherent stack. Laribee and Qlerify cover domain vocabulary. Fowler covers language architecture and evolution. Joshi applies that architecture to LLM generation. ByteByteGo supplies the economic boundary: detailed domain modeling is valuable when complexity justifies it, not as default ceremony.

1. A DSL Gives the LLM a Smaller Target

If I ask an LLM to "build Wordle," I am not making one request. I am quietly delegating a dozen product and architecture decisions.

Wordle is a word puzzle built around a simple loop. The player has six attempts to guess a five-letter word. A green tile means the letter is correct and in the correct position. Yellow means the letter exists in the target word but belongs somewhere else. Gray means the guess did not earn a match for that letter.

That sounds complete. It is not.

The hidden duplicate-letter bug

Consider this apparently clear instruction:

Score every letter as green if it is in the right place, yellow if it appears elsewhere in the target word, and gray if it does not appear in the word.

The sentence hides the hardest rule. Suppose the target is APPLE and the player guesses POPPY. The third P is green because it is in the correct position. The first P is yellow because one unmatched P remains elsewhere in the target. The fourth P must be gray because the target has no unused P left. The same guessed letter receives three different results.

Now the missing decisions become visible. Should exact matches be scored before partial matches? Can one target letter satisfy two guessed letters? Does an invalid dictionary word consume an attempt? Can the game accept accented characters? Is the target list different from the allowed-guess list? What happens after the sixth failed attempt?

An unconstrained LLM does not remove those questions. It answers them without telling me, or implements whichever interpretation happened to be statistically convenient.

Design rule: Score exact matches first, then consume each remaining target letter at most once when assigning partial matches.

Plausible code versus a constrained DSL

Plausible LLM output with a hidden bug

def score_guess(target, guess):
    result = []
    for index, letter in enumerate(guess):
        if letter == target[index]:
            result.append("green")
        elif letter in target:
            result.append("yellow")
        else:
            result.append("gray")
    return result

Hidden defect:letter in target never consumes matched letters. For POPPY against APPLE, the fourth P becomes yellow instead of gray.

Constrained DSL with an executable example

game: word-guess
word_length: 5
max_attempts: 6
scoring:
  exact_matches_first: true
  consume_target_letters_once: true
examples:
  - target: APPLE
    guess: POPPY
    expected:
      - yellow
      - gray
      - green
      - gray
      - gray

Constraint: the rule and the edge case are explicit. A validator can reject an implementation that produces a second yellow P.

--

The Python is plausible, readable, and wrong. Its membership check ignores letter counts. The DSL does not become reliable merely because it uses YAML. Reliability comes from the semantic model behind the fields and the executable example that the validator can enforce.

A domain-specific language removes choices that should never have been open. It exposes valid guesses, scoring order, attempt limits, state transitions, and edge cases while leaving rendering, persistence, and framework details outside the language.

Fowler's core principle, paraphrased: A DSL should expose the language of one domain while hiding implementation choices that its users should not need to repeat.

This is where Domain-Driven Design becomes useful. DDD organizes software around the language and rules of the problem being solved. Its ubiquitous language connects conversation, documentation, code, tests, and APIs. Qlerify adds an important constraint: that vocabulary should evolve as understanding improves and should appear in code, not remain trapped in a meeting document.

Terms such as Guess, AcceptedGuess, ExactMatch, PresentElsewhere, NoRemainingMatch, AttemptUsed, GameWon, and GameLost stop being loose English and become defined operations inside a bounded context. A bounded context is simply the boundary within which those words have one agreed meaning.

Once that vocabulary exists, the LLM no longer has to invent the domain while generating the implementation. It maps the request onto concepts that already have meaning.

Internal or external DSL?

An internal DSL is built inside an existing programming language. It may appear as a fluent API or typed configuration that the compiler can check before execution. It inherits the host language's editor, test tools, type system, and runtime. For developer-authored workflows, it is usually my default starting point.

An external DSL owns its syntax instead of borrowing another programming language. It can be cleaner for non-developers or portable across programming languages, but somebody must build and maintain its parser, diagnostics, editor support, versioning, migrations, and security model.

Fowler's book goes much further than this basic distinction. It treats a DSL as a small language system with recurring design problems. A Symbol Table resolves names such as a word-list identifier into the correct underlying object. A Construction Builder turns readable instructions into semantic objects. An Expression Builder combines smaller rules into a larger rule without exposing the implementation. A Progressive Interface limits which instruction can legally follow another, so invalid sequences are difficult or impossible to express.

Those patterns matter for LLMs because each one closes a category of accidental choice. The model does not need to invent how names are resolved, how rules are assembled, or which sequence of calls is legal. It receives a smaller set of moves and clearer feedback when it chooses the wrong one.

The attractive syntax is the smallest part of the bill. The semantic model, diagnostics, ownership, and migration path are where the money goes. That part usually becomes visible after the demo, when the first exception arrives.

2. A Validator Turns AI Coding Into an Engineering Loop

Reliability should not depend on writing longer prompts that ask the model to remember every rule. Prompt engineering can guide output. A DSL with validation can reject invalid output.

That is a much stronger arrangement.

The validator might be a compiler, type system, schema, policy engine, linter, test suite, simulator, or a combination of them. What matters is that it returns precise failures in the language of the domain:

WORD_014: target length must equal word_length
GUESS_021: guess is not present in allowed_guesses
SCORE_006: exact matches must be consumed before partial matches
STATE_009: a seventh attempt is not legal

Now the agent can run a tight loop:

  1. Translate the request into the DSL.
  2. Validate the generated program.
  3. Read the domain-level error.
  4. Repair the program.
  5. Run deterministic tests.
Reliability principle: Generation becomes engineering when invalid output returns a precise, machine-actionable error.

Joshi demonstrates this pattern with Tickloom, a framework for testing distributed systems. Tickloom fixes decisions about logical time, message handling, storage, networking, and event ordering. The model implements protocols through an established vocabulary of replicas, messages, handlers, faults, and deterministic ticks rather than inventing the concurrency model at the same time.

That restriction does not make the problem trivial. It makes the remaining problem visible.

This is the difference between reviewing a generated system and reviewing a domain statement. In unconstrained AI coding, business rules arrive mixed with persistence, concurrency, framework conventions, and incidental implementation choices. A strong semantic model fixes or isolates much of that variation. The review can focus on the question that matters: Is this the correct rule?

Validation still has a hard limit. A compiler can prove that a game definition follows the grammar. A test can confirm that POPPY is scored correctly against APPLE. Neither can decide whether the word list is fair, whether the feedback is understandable to a color-blind player, or whether the game is actually enjoyable.

Validation proves conformance, not quality. The machine checks the rules we encoded. Humans remain responsible for whether those rules deserve to exist.

3. The DSL Becomes Durable Product Memory

I also care about DSLs because prompts are terrible institutional memory.

Prompts contain half-finished thoughts, forgotten assumptions, contradictory instructions, and context that made sense only during one conversation. Months later, the code remains but the chain of decisions that shaped it is difficult to reconstruct. Prompts are useful working context. They are weak long-term specifications.

A well-designed DSL changes the durable artifact. The maintained object is no longer the prompt; it is the domain program and the semantic model that gives it meaning.

That matters because software development is not one act of generation. Rules change. New game modes appear. Accessibility requirements improve. A daily challenge may need a different word list. A multiplayer version may introduce timers, rematches, or shared results.

In a Wordle-style game, these words cannot be allowed to melt into each other:

  • SubmittedGuess means the player attempted to enter a word.
  • AcceptedGuess means the word passed validation and counts as an attempt.
  • ExactMatch means the letter and position both match.
  • PresentElsewhere means an unused copy of the letter exists in another position.
  • GameWon and GameLost are terminal states. No later guess is legal.

Collapsing those rules into containsLetter: true works until the first repeated letter appears. Green, yellow, and gray can all be correct for the same character in one guess. The shortcut reduces code initially and transfers complexity into debugging and future changes.

The DSL preserves those distinctions in an executable form. The LLM can read the current language, add a seven-letter mode, generate duplicate-letter tests, explain a validation failure, or produce documentation for the scoring rules. The context survives the model, the prompt, and the person who happened to be online when the first version shipped.

This is also why I do not think every designer, game writer, or content editor needs to write the DSL directly. That is not the goal. The goal is to have one compact artifact that people can challenge without translating the entire game back from code.

How to Use DSLs With LLMs in Practice

I am not arguing that every product needs a new language. The easiest mistake is to design a grand DSL for a domain the team barely understands. Early product discovery is messy because the vocabulary is still moving.

A better sequence is:

  1. Collect real scenarios. Use actual policies, edge cases, failures, user requests, and operational exceptions.
  2. Name the domain. Identify actors, commands, events, states, values, permissions, and invariants.
  3. Build the semantic model. Separate domain meaning from UI, parsing, storage, and transport.
  4. Start with a narrow internal DSL or schema. Reuse the host language and toolchain before funding a miniature language platform.
  5. Add deterministic validation. Write errors in domain language so both humans and agents can act on them.
  6. Give the LLM examples and boundaries. Let it generate programs inside the language, not redesign the architecture on every request.
  7. Feed failures back into the model. If the same ambiguity keeps returning, repair the vocabulary or semantics instead of adding another paragraph to the prompt.

The LLM has two jobs across this sequence. During discovery, it is a design partner: proposing names, counterexamples, alternative boundaries, and awkward cases. After the language stabilizes, it becomes a natural-language interface to a system whose legal moves are already known.

Confusing these phases is expensive. If you freeze the DSL too early, you turn shallow assumptions into infrastructure. If you never stabilize the domain, every prompt pays the same reasoning cost again.

When a DSL Is the Wrong Tool

Do not build a DSL to make the architecture appear sophisticated.

A DSL is usually a poor investment when the task is one-off, the domain is still changing daily, ordinary code already expresses the intent clearly, no team owns language evolution, or there is no meaningful validator behind the syntax. A YAML layer does not turn simple CRUD into a core domain.

The accessible introduction to ByteByteGo's DDD guide makes the economic boundary clear. DDD is most useful when the problem is complex, keeps changing, and creates costly misunderstandings between people and software. It is not a requirement for every small application. The InfoQ DDD collection is useful as a map toward case studies about improving domain models and deciding where one software service should end and another should begin. It is an index, not evidence for any one claim, so I treat it as a research route rather than a result.

The strongest candidates share three characteristics:

  • The activity is repeated.
  • The rules have real economic or operational consequences.
  • A machine can check important parts of correctness.

Game rules, pricing rules, workflow orchestration, infrastructure definitions, test scenarios, access policies, and network behavior simulations often qualify. A settings file with four booleans probably does not.

Frequently Asked Questions

Is YAML a domain-specific language?

Not by itself. YAML is a carrier syntax. It becomes part of a DSL when a schema, semantic model, domain vocabulary, and execution or validation behavior give its fields precise meaning.

Can DSLs stop LLM hallucinations?

They can reduce irrelevant choices and reject invalid output. They cannot guarantee that the domain model is complete or that the requested business rule is sensible.

Do I need to build a new programming language?

Usually not. A typed builder, fluent API, schema, policy format, or constrained configuration layer may provide most of the value with far less tooling cost.

What is the relationship between DDD and DSLs?

DDD helps discover the language, boundaries, entities, values, and invariants of the domain. A DSL exposes a narrow, executable way to express operations within that model.

Are prompts still useful once a DSL exists?

Yes. The prompt becomes the flexible human interface. It explains the desired outcome. The DSL becomes the durable and verifiable representation that the system maintains.

The Real Division of Labor

LLMs are good at crossing the messy gap between how people speak and how systems are instructed. DSLs are good at making the destination precise. That combination is much closer to how I want to build AI-assisted products.

I treat the prompt as the conversation, the semantic model as the understanding, the DSL as the contract, and the validator as the enforcement layer.

The model will keep getting faster. Speed is not the part I am worried about. I want AI to make fewer undefined decisions, not merely write more code.

The hard work is still deciding what words mean, where a rule is valid, and which failures are unacceptable. I want that knowledge in a language the machine can check. Then the model can move quickly inside boundaries I actually intended.