Ubiquitous Language Inside a Bounded Context
What This Concept Is
The ubiquitous language is the single, precise vocabulary used by domain experts, designers, developers, and the source code inside one bounded context. The word "ubiquitous" means ubiquitous within the context -- not across the whole organization.
A ubiquitous language has four discipline rules:
- One term, one meaning, inside this context. If
ordermeans two different things, the context is too wide. - Domain experts and code use the same words. If the expert says "shipment" and the code says
ParcelDto, there is drift. - Conversations, diagrams, emails, and tests all use the same vocabulary. Otherwise the glossary is aspirational, not enforced.
- When language conflicts show up, split the context. Prefixing (
billing.Invoicevsshipping.Invoice) is a symptom; the cure is recognizing two contexts.
Why It Matters Here
The ubiquitous language is the test for whether a bounded context is drawn correctly:
- if you need to qualify every term with its subsystem, the context is too wide
- if a conversation with an expert uses the same word for two things, the context is too wide
- if two teams use a shared term but mean incompatible things, they are in two contexts whether they know it or not
The entire context-mapping chapter (Cluster 2) assumes that each context carries one well-defined language; all the integration patterns (ACL, OHS, published language) exist to translate between them.
Concrete Example
Case: Parcel Shipping Co. -- the word "shipment" across contexts
| Context | Meaning of shipment | Key fields | Lifecycle |
|---|---|---|---|
| Pricing | A quote request: dimensions, origin, destination, service. No identity yet. | weight, dimensions, zone, service_class, declared_value | Created on quote, discarded if user does not book |
| Shipping | A booked intent to ship: the label has been generated, the parcel has an identity (AWB), but no scan has occurred yet. | awb, carrier, label_pdf, booked_at, deadline | booked -> labeled -> handed_off |
| Tracking | An active parcel moving through the carrier network, identified by AWB, updated by scan events. | awb, current_status, last_scan_at, location, estimated_delivery | in_transit -> out_for_delivery -> delivered / returned |
| Billing | A billable line item on an invoice: frozen rate, carrier, customer. | awb, rated_amount, customer_id, invoiced (bool) | billable -> invoiced -> paid / disputed |
All four contexts use the same word shipment. All four mean something different. Each meaning is internally coherent. The cost of collapsing them into "one shipment model" would be a 40-field class with fields that are nullable at different moments of the lifecycle.
What the language statement looks like on the wiki
A bounded context's wiki page starts with a language statement. Two examples:
Pricing Context language statement (excerpt):
In the Pricing Context, a
shipmentis a quote request, not a booked parcel. It has no AWB and no carrier yet. Arateis the numeric output of the pricing rules for a specificcarrier+service_classon a givenshipmentat a specificas_oftimestamp.rateis never persisted beyond the quote TTL. Acontractis the customer-specific override list that can cut, cap, or replace carrier list prices. We do not use the wordinvoicehere; money we compute is called arate, never anamount due.
Tracking Context language statement (excerpt):
In the Tracking Context, a
shipmentis an AWB plus its unified event stream. It does not have a price, a carrier account, or a customer; those belong to Shipping and Billing. Ascanis a single raw record received from a carrier. Astatusis a value from our unified enum (booked,in_transit,out_for_delivery,delivered,exception,returned). Status is derived from scans, never set directly. The worddeliveryrefers to the event, not to the person or address; the address isdestination, fixed at shipping time.
These statements read boring on purpose. They are the definition that every PR, every email, every test name, and every public API description has to match.
What it looks like in code
In the Pricing Context the aggregate root is Quote (or Shipment used in the pricing sense):
class Quote:
id: QuoteId
dimensions: Dimensions
zone: Zone
service_class: ServiceClass
declared_value: Money
carrier_candidates: list[Carrier]
def rate_for(self, carrier: Carrier) -> Rate: ...
In the Tracking Context the aggregate root is Shipment but the code carries none of the pricing fields:
class Shipment:
awb: AWB
scans: list[Scan]
status: Status
def register_scan(self, scan: Scan) -> list[DomainEvent]: ...
Same word. Two different classes in two different codebases (or two different modules in one monolith). No "unified shipment model."
Common Confusion / Misconception
"The ubiquitous language is the company's glossary." No. It is this context's language. Cross-context terms must either be translated at the boundary or accepted as unrelated. The failure mode is a 400-term "enterprise glossary" that the code does not actually use.
"We should enforce one vocabulary across all teams." That is exactly what DDD argues against. Each bounded context gets its own language. Cross-context integration translates at the wire (see concept 5). Forcing one vocabulary collapses real conceptual differences into name collisions.
"If domain experts disagree, pick the senior one." If domain experts disagree, you likely have two contexts. Their disagreement reveals the boundary.
"The ubiquitous language is informal -- marketing words do not matter." They do, because marketing words become code. If the sales deck says "parcel" but the code says Shipment, either marketing or the code is drifting. Pick one and enforce it in both places.
"Technical words are not part of the language." A naming split between domain language (Shipment) and technical language (Repository, Handler, Dispatcher) is fine and expected. What must match the domain is the entity, event, and command names -- not the plumbing.
How To Use It
To write a ubiquitous language for a context:
- Sit down with a domain expert and the names of the main entities.
- For each key term, write: the term, its definition in this context, an example sentence, and one example of what it is not.
- If a term has two definitions, stop. You have two contexts, not one. Name the split.
- Write the result as the wiki page's "Language" section.
- Enforce it: class names, event names, API resource names, and commit-message vocabulary must use the language. PR review asks "does this name match the language?"
- Revisit every quarter; evolve when the domain shifts.
Check Yourself
- In a system you know, name a word that means two different things in two different modules. Does this reveal a bounded-context boundary?
- Why is a single enterprise-wide glossary a DDD anti-pattern rather than a helpful artifact?
- If the domain expert uses "shipment" casually to mean both "quote" and "parcel in transit," what is your next question?
Mini Drill or Application
For the Parcel Shipping Co. Tracking Context, produce a 10-term language statement:
- Pick 10 key terms:
shipment,scan,status,location,carrier,delivery,exception,return,ETA,journey. - Write one sentence defining each, in this context.
- For two of them, write a sentence showing what the word does not mean here.
- Find one pair of terms that an outsider might confuse and write the "these are different because..." clarifier.
Keep the statement to one page. It should be readable by a new domain expert in 5 minutes.