Building a Ubiquitous Language is one of the foundational precepts of Domain-Driven Design (DDD). DDD strives to bridge the gap between business complexity and software implementation by modeling a system on concepts, abstractions, and terms from the domain it is intended to serve. On many projects I’ve been part of, the software teams will translate business terms into different terms used in the codebase: The sales team says “orders,” but the dev team calls these “invoices”; The shipping team uses the term “pick list” everywhere, but in the codebase, we see “packing slip.” These constant translations inhibit communication between the business and IT. Concepts get muddied, implementations become sub-par, and general effectiveness erodes over time.
To combat this, business and IT must strive towards a common, ubiquitous language. These terms must be repeated, reinforced, and refined so that the systems built are exactly the systems needed, reflecting a shared vision of a model that both business and IT can agree upon. When IT and business use the same words for the same meanings in the same context, they move past superficial understandings to deeper insights and value delivery. However, one common mistake I see teams make is trying to apply the same term universally across the entire enterprise, especially when moving away from single, monolithic systems to smaller, more granular services. If we find a single representation of a User, a Customer, or an Order in our monolith, surely this would mean we would have a single service or boundary that owns that concept? In reality, a single owner for a single foundational aspect can often lead to anemic, inefficient systems.
When a “Product” isn’t a Product
An e-commerce company looking to modernize their legacy online ordering system wanted to apply DDD principles to their new design. They wanted to drive towards a ubiquitous language, as the existing terms in the legacy system did not have any real business-significant names (a Customer was represented as a Person, an Order was a Form). Starting with a clean slate, they wanted to ensure they used the right names for the right concepts, and build towards this language with the business. Their first pass was to look at the foundational piece of the system (Product), and build a boundary around that concept so there was a single owner of the Product data and Product behavior. Each service that needed Product information queried the Product service to retrieve it. The intent was that the service owned all Product information. When the developer team talked to each business group, they all used the term “Product,” so it seemed natural to solidify the language and build a service around that concept. However, cracks quickly appeared in this model: Although all business groups used the term “Product,” each had different needs and requirements for a Product, and the development team struggled to satisfy everyone. This was most easily illustrated when data changed over time. A Product first gets shown in the Catalog, added to a Cart, submitted as an Order, and then finally shipped in Fulfillment:At each step along the customer journey, the service refers back to that common Product. An interesting side effect emerged when the Product price changed. How does that show up in each service, if the customer has already engaged before the price change?If I’m a customer, I’d probably be rather upset if I see the price increase while the Product sits in my cart. If it decreases, probably not, but it would be a bit disconcerting. And clearly, a Product price can’t change after Fulfillment—we’ve already charged the customer! To mitigate this, the team tried to assign date ranges to the Product, so that when I asked the Product service for a price, I also had to say “as of so-and-so date.” The Product service then had to include history information on prices, so that it could reflect a price as of some point in time. But even this wasn’t enough! Sometimes discounts were applied by the Order team because of inventory issues or customer preferences, and for legal reasons, the Cart price couldn’t change once the item was placed in the cart. Something is clearly wrong here, but the team was set on building towards a ubiquitous language, so how can we fix this? The mistake was assuming that the boundary of ubiquity was much larger than could actually be useful.
Applying Boundaries to Ubiquity
For as long as humans have had language, we’ve argued over the meaning of words. Even though we’ve written down definitions for centuries, when we apply those meanings to business, not every term can be so cut and dry. The purpose of ubiquitous language is not to build towards singular meanings, but only to build common language insomuch as it is useful for modeling and development. If the term ceases to have a common meaning, it’s no longer useful as part of our common language. In our e-commerce example, the team made the mistake of assuming that just because everyone in the organization used the term “Product” that a) it meant the same thing to everyone and that b) discrepancies represented mistakes or misunderstandings. The business more or less understands what it’s talking about, but may not need to agree on a universal truth in order to successfully conduct business. Does a Product in a catalog need to be the exact same concept in Fulfillment to be successful? Can it diverge? What we found was that there was no single truth of a Product, that each step in the customer journey applied a different perspective of what a “Product” is:In our matrix-like view of a Product, we realize that there is no central concept or model of a Product and that each service has its own internal definition. Some pieces of information do remain relatively static over time (e.g., images), but others may not (e.g., price). When we see that different groups in the organization use the same terms, but data or behavior changes over time—especially over a value stream/customer journey—it’s a great indication that we need to break a concept up amongst multiple groups, or “bounded contexts,” as DDD dictates. When we do so, we allow each bounded context to apply its own meanings and rules for those model terms, removed from the worry of affecting any upstream or downstream services. We still need the translation between services, and anti-corruption layers to bridge each gap. Inside each service boundary, our terms, models, and language will remain consistent, adhering to the values of a ubiquitous language. In the end, a ubiquitous language only becomes truly valuable when we constrain the boundary of ubiquity.
Did you catch our webinar on microservices messaging? Check out this follow-up post from Jimmy: Your NServiceBus questions answered