Ubiquitous Language is prompt engineering that humans can read

· Menelaos Vergis ·

8 min read Original article ↗

The artifacts Domain-Driven Design gives to human teams turn out to be exactly the context AI coding assistants need.

Ubiquitous Language is prompt engineering that humans can read

Jul 14, 2026

You never pin down how long an inventory reservation should last, so the assistant decides for you: the generated code expires it after 15 minutes. A perfectly plausible number, and not the one your business runs on. It compiles, the tests pass, and code review sails right past it, because a wrong rule is hiding inside perfectly correct syntax. That single failure mode is the whole reason this article exists: an LLM will happily fill the gaps you leave with confident, well-formed guesses, and the only defense is making the domain explicit enough that the rule is something the code gets checked against, not something the model was left to invent.

That discipline, making the domain explicit, already has a name. It's Domain-Driven Design. And it turns out the artifacts DDD makes you produce are exactly what an AI coding assistant is starving for.

This isn't theoretical. I kept catching Claude inventing its own domain vocabulary in my Shopify apps, quietly renaming the concepts my team had already agreed on. Once I started treating the Ubiquitous Language as part of the prompt rather than an afterthought, the quality of the generated code changed sharply.

Why DDD fits LLM-assisted development

An LLM's output quality is bounded by the quality of the context it's given. DDD is a discipline for manufacturing exactly that kind of context. Every DDD artifact doubles as prompt material.

A quick example to make this concrete, and one I'll reuse throughout. It's the catalog and inventory side of a store. The team fixes a couple of words up front. An InventoryReservation is a temporary claim on stock, created when a shopper starts checkout; it expires after a few minutes so the stock goes back on sale. And to HideProduct removes a product from the storefront without deleting it. Two plain words, used the same way by everyone.

Four concrete ways this shows up:

1. Ubiquitous Language ↔ prompt context. The single biggest lever on AI code quality is unambiguous vocabulary. Because the team fixed those words, the model generates Product.Hide() instead of guessing at Product.Delete() or Product.Deactivate(). The word "hide" was chosen deliberately over "delete," because a hidden product still exists and can be republished, so nobody, human or machine, has to guess whether the record survives. The glossary you write for the team is the same glossary the AI needs. You were going to write it for humans anyway, so now it pays double.

2. Bounded contexts ↔ scoping the session. LLMs degrade when you dump an entire system into the prompt; attention spreads thin and boundaries blur. A bounded context is a natural unit of work: "we're in the Catalog context today; here's its language and its rules; ignore Payments." That maps cleanly onto one focused session or one subagent, which is exactly how you get the model to stay on-target.

3. Behavior-rich models ↔ code the AI can't silently corrupt. Now a second rule enters, and here are the words for it: a product is OutOfStock when its available stock hits zero, and it can't be published in that state unless ContinueSelling (the backorder policy) is switched on. When that invariant lives inside the object (Product.Publish() checks it), there's one guarded doorway. When it's scattered across services (the anemic style), any generated line can reach in and set Status = "Active", publishing an unbuyable product and skipping the rule. Concentrating rules in one place shrinks the surface where an AI (or any teammate) can quietly break things.

4. Tests-as-spec ↔ an executable contract. DDD's invariants translate directly into tests: "cannot publish an out-of-stock product unless ContinueSelling is on." Have the assistant write those tests first, and you've given it an executable definition of "correct" to code against, which is far more reliable than prose alone.

The traps hiding in Claude + DDD

Here's the honest part. An LLM's default behavior is the enemy of good DDD, because the training data is full of the patterns DDD warns against. Know these before you start:

  • It defaults to anemic CRUD. Ask for "a Product class" cold and you'll often get public getters/setters plus a ProductService holding the logic, the exact anti-pattern. You have to explicitly ask for behavior on the object and private setters.

  • It invents vocabulary. You say HideProduct, it writes DeactivateProduct. It's plausible, it compiles, and now your code and your team speak different languages, one where "hide" and "deactivate" might mean the same thing or might not. This drift is silent and compounds.

  • It reaches across boundaries. Left unscoped, the model will happily wire Catalog directly into Payments and grow a god-object, because it doesn't feel the seam you drew.

  • It over-engineers. Aggregates, repositories, domain-event buses for a three-table admin panel. The patterns are in the training set, so it applies them whether or not the domain warrants it. DDD without the "when not to" is just ceremony.

  • It generates plausible-but-wrong invariants. This is the invented-expiry trap from the opening. It's the most dangerous item on this list precisely because a wrong rule hides inside perfectly correct syntax, so any review that only asks "does it run?" misses it. The defense is DDD's own habit: pin the invariant down in the glossary and in a test, so the number is something the code is checked against, not something the model guessed.

  • Context goes stale. Over a long session the model drifts from the agreed language. Re-ground it.

None of these are reasons not to use Claude for DDD. They're reasons to steer, which is what the workflow is for.

A practical workflow for LLM-driven DDD

  1. Put the Ubiquitous Language in a CLAUDE.md (or your assistant's standing-instructions file). List each term, its precise meaning, and a "not to be confused with." Now the language is auto-loaded as context on every session.

  2. Work one bounded context at a time. Keep each context's language and rules in its own CLAUDE.md (or equivalent), so the assistant automatically loads the right vocabulary the moment you work there. One context per session or subagent keeps its attention where the seam is.

  3. Make it restate the model before it codes. Ask: "Before writing anything, list the invariants Product must enforce, in your own words." This is teach-back in reverse: drift and misunderstandings surface before they become code, when they're cheap to fix.

  4. Bake the behavior-rich preference into standing instructions. Put it where it persists: a coding-standards line in CLAUDE.md, or a reusable skill. Something like: "domain objects that enforce rules should keep those rules on the object and expose intent-revealing methods like Publish() and Hide() rather than open setters." Keep it a preference, not a mandate; it's for the objects guarding real invariants, not value objects, DTOs, or config carriers, which are fine with plain properties. Forcing every setter private across the whole codebase just swaps one anti-pattern for ceremony.

  5. Tests first, as the contract. Have it write the invariant tests (Cannot_publish_when_out_of_stock_unless_continue_selling) before the implementation. You review the spec, which is small and readable, rather than hunting bugs in the implementation later.

  6. Review against the language, not just correctness. In review, ask two questions: does it work, and does it speak the Ubiquitous Language? A method named Process() is a smell even if it passes, because it means a concept wasn't named.

The loop is virtuous: a sharper model makes sharper prompts, sharper prompts make better code, and the code you read back teaches you the model. Human, domain expert, and AI all reading one language.

Drawing the line: which parts of DDD to actually use

DDD is not all-or-nothing, and this is the part beginners (and eager LLMs) get wrong most often. The useful frame is cheap vs. expensive.

Cheap, use nearly everywhere:

  • Ubiquitous Language. Costs only discipline. Pure upside, even on a small project. And with an AI, it directly improves generated code.

  • Behavior-rich objects (no anemic models). This is just good object-oriented design; it's simpler than data-bag-plus-service, not more complex.

  • A few value objects for the obvious things like Money and SKU. Cheap in C# with records, and they delete whole classes of bugs.

Expensive, adopt only when the domain earns it:

  • Carefully designed aggregates and consistency boundaries.

  • Context maps across multiple teams and contexts.

  • Repositories, separate application/domain layers, domain-event infrastructure.

The deciding factor isn't how long the project takes. It's how rich the rules are. A two-day feature encoding god-inspired pricing rules may want value objects and behavior-rich models; a three-month CRUD panel may need almost none of it. Tell your assistant where the line is, or it will scaffold the whole cathedral for a garden shed.

The bottom line

LLMs reward whoever gives them the clearest picture of the problem. DDD is a decades-old discipline for producing exactly that picture: a named language, drawn boundaries, rules stated out loud. Pairing them isn't a hack; it's two things that want the same input.

Give the model a Ubiquitous Language, clear boundaries, and behavior-rich models. Verify the rules with tests. Let the domain decide how much DDD you need.

Because in the end, Ubiquitous Language is prompt engineering that humans can read, and you were going to write it anyway.

Tags