Domain-Driven Design
Ubiquitous Language
The words we share with the people who live the domain, used in speech and in code.
A ubiquitous language is the vocabulary used to discuss the model and change the code. If a conversation says Envelope while the class says MessageWrapper, every change begins with a translation. If the code says Site after the model has moved to Store, the old model is still deciding the work.
It is not a glossary written once. The language changes when a name stops describing the rules, and the boundary records any old names that still have to cross it.
A name makes a promise
Hansom calls the stable string for a CLR message type a wire name. That phrase appears in the project description, the model, and the tests. The implementation then has to keep the promise: an incoming wire name may be known or unknown.
MessageTypeCatalog.Lookup returns that distinction in the model:
public MessageTypeLookup Lookup(MessageType name)
{
ArgumentNullException.ThrowIfNull(name);
return _byName.TryGetValue(name.Value, out var clrType)
? new KnownMessageType(clrType)
: new UnknownMessageType(name);
}Lookup answers in the model's words: the wire name is known or unknown. A dictionary miss would only say how a collection failed.
UnknownMessageType is useful language. It tells the caller what happened in the bus. KeyNotFoundException would only tell the caller how a dictionary failed.
public record UnknownMessageType(MessageType Name) : MessageTypeLookup;The type name is the distinction. UnknownMessageType tells the caller what happened in the bus.
The test is not whether a type sounds business-like. It is whether its name preserves a distinction the model needs.
The language can correct the payload
Here is an example from an editor payload that still contains siteId fields. One queue field already referred to a Store and was corrected to populate StoreIds. Other legacy values still arrive through SiteIds while that contract is being retired.
The current validation path keeps that compatibility name at the edge, then combines both sets before validating Stores:
var storeIdsToValidate =
new HashSet<string>(references.StoreIds, StringComparer.Ordinal);StoreIds are already in the current language. They enter the set as Stores.
foreach (var siteId in references.SiteIds)
storeIdsToValidate.Add(siteId);SiteIds still arrive from the old contract. They are translated in, not modelled as a second kind of place.
if (storeIdsToValidate.Count > 0)
await ValidateStoresAsync(storeIdsToValidate, userGroupId, errors, ct);Validation is ValidateStoresAsync. The old string did not earn a Site in this context.
The old string did not earn a Site in the current model. It earned a translation. Later payloads can say storeId; older ones can still be read without making both words true inside the context.
Shared means used
A vocabulary becomes ubiquitous through use: questions, code, tests, and corrections all employ the same distinctions. Hansom’s tests ask for an UnknownMessageType. The editor fix validates legacy site identifiers as stores. In both cases the language changes behaviour; it is not a list pinned above a desk.
The words are shared inside a bounded context. Across the line, another model or an old contract may use different words. Translate at that line, rather than carrying the disagreement through every type.
Once the words are shared, the next job is deciding which parts of the problem the model should keep. That is Modelling.