I want to discuss a topic near to my heart, a topic I believe to be the crux of effective software design. No, it’s not a new functional language, it’s not a fancy new framework, it’s not a how-to guide to do micro-services, nor a quantum leap in the field of machine learning. It’s much simpler. It’s about names. Names define us. They give life to abstract ideas and concepts and yet also stand in for real, physical objects. They’re language concepts, but more than that, they’re units of meaning. When used precisely, names enable shared understanding and smooth teamwork among people. Software development is fundamentally a human endeavor. No amount of technical computing breakthroughs will change the fact that software development is still the arduous task of assembling people from a kaleidoscope of different cultural & linguistic backgrounds, then throwing them together to build an arbitrarily complex product in a rapidly shifting competitive landscape. The landscape is littered with software projects that began ambitiously, but got lost in a towering mess of fragile code. Many teams have found themselves consistently foiled by bugs and system design inconsistencies that originate from a failure to communicate meanings, concepts and ideas well. It’s no wonder that developing reliable, successful software is more art than science.
Let’s imagine an imaginary scene from your company, Delorean (the Uber for time travel). You are a software engineer on the team responsible for writing software systems that handle e-commerce payment processing for your users, who pay for their rides from your company’s time-travel ride sharing service. You are discussing the implementation of a new set of features with your product owner (PO), and it unfolds like this:
PO: Our next big project is to update our driver app to show rider locations on the timeline map. You: And when do these riders show up on the timeline screen? PO: When the driver turns on the app and signals that she’s driving. You: OK, so that means when the app boots up and theDriverStatus
service receives a POST we’ll need to simultaneously fetch references from theHailingUser
service based on time locality. PO: Um…
Your poor product owner, removed from the direct details of implementation in your code, gets a hazy explanation of what’s actually going on. He vaguely remembers that the DriverStatus subsystem vaguely corresponds to drivers who use the mobile app and the HailingUser subsystem deals with riders who want rides, but he always gets them mixed up. Your system’s naming conventions are getting in the way, and they don’t correspond to any real-world concepts that he’s familiar with. Come to think about it, this same confusion played out among the team in the last iteration planning meeting, where you discussed the intricacies of a specific story:
PO: In this story, we’re going to add a coupon box to the checkout flow. You: [Thinking out loud] Hm… would that mean we add a/coupon
route to thecheckout
API? We could POST to it and then it will createCoupon
s in the backend. Teammate: Wait – I think we call themDiscounts
in the backend. And the checkout API is technically implemented by theRideCommerce
service. You: Oh right, I forgot. Let’s make the route/coupon
and it’ll create aDiscount
object. And in this story, let’s just remember that the Checkout API really refers to theRideCommerce
service.
A completely different implementing engineer, of course, doesn’t read the note in the project backlog story (who has time to, anyways?). In the course of implementation, he gets tripped up in semantics and spends the better part of a half day re-implementing the Checkout
flow as an entirely new service, before realizing his mistake in code review and backing out his changes. Months later, a new colleague is tasked to fix the link in the checkout flow, but files an incomplete fix because she was not aware of the fact that Coupons
actually meant Discounts
. The bug makes its way to production, where the code begins causing havoc in your critical checkout systems.
Eric Evans’ book Domain-Driven Design introduces the concept of a Ubiquitous Language – a shared, common vocabulary that the entire team shares when discussing software. This “entire team” is made up of designers, developers, the product owner and any other domain experts that exist at the organization. Take note that it is important that your team have a domain expert. Your product owner may be your domain expert (and typically is). However, you may have other domain experts such as:
Get your team together – the domain experts and the technical experts – and set them about creating a document that tracks your team’s language. This is the living document of all the terminology your team uses – along with all its definitions. This is known as the Glossary: From now on, use only the term definitions listed here in your stories both in person AND in your source code. Be explicit about how you use your language! I’ve been on many projects where the sloppy usage of a term from project inception led to the usage of that term in the code – codifying that messy, slippery term throughout the life of the project. The purpose of the Ubiquitous Language is to get the whole team on the same page – what each thing means, and bridge the gap between the jargon used on the development team and the customers. Your Glossary is a living document. It is meant to be living – either on a continually updated Google Doc or a wiki page. It should be visible for all to see – you should print it out and post it on the walls!
A great time and place to start using this new Ubiquitious Language is in your everyday conversations with your domain experts, or with the technical team. Let’s listen in on a planning meeting with the product owner:
You: So when a User logs into the app and broadcasts that they’re ready to drive… PO: You mean Driver. When a Driver logs in. You: Right. Good catch.
The little correction seems a little silly (after all, you both know only Drivers use the broadcast feature of the app), but the laser focus on using the right words means that your team is always on the same page when talking about things. As your team starts using the defined terms in the Glossary, the same words start to slip off your tongues, making working together easier than ever. Later that afternoon, your teammate taps you on the shoulder:
Teammate: I’m about to start working on the checkout flow. I suggest we rename the Discount class to Coupon. You: Great idea. That way, we aren’t tripped up by the naming mismatches in the future.
Congratulations! Your team is making its first steps to Ubiquitous Language nirvana: your code needs to reflect your domain language.
Teammate: I do have a question about the coupon, though. Do you think it’s applied to the BookingAmount, or is it added? PO: [Overhearing conversation] You had it right. It’s applied.
You and your teammate then go and update the glossary, scribbling an addendum on the wall (or updating your wiki): Future teammates will thank you for your precision with your terms.
Your teammate and you then walk over to her desk; as a pair you proceed to refactor the existing account code. We’ll use Ruby for the sake of this example. In the beginning, the Checkout code looks like this. A pretty straightforward implementation of the parts of the checkout flow that apply a coupon. https://gist.github.com/andrewhao/4783e3636fb1e47adfa195b749986733#file-pass_1-rb
You take a first pass and rename the Discount
class to Coupon
. https://gist.github.com/andrewhao/4783e3636fb1e47adfa195b749986733#file-pass_2-rb
Now there’s something funny here – your domain language suggests that a Coupon is applied to a BookingAmount. You pause, because the code reads the opposite – “A Coupon calculates its amount for a BookingAmount”.
You: How about we also refactor the calculate_amount_for
method to reflect the language a little better? Teammate: Yeah. It sounds like the action occurs the other way – the BookingAmount is responsible for applying a Coupon to itself.
In your next refactoring pass, you move the calculate_amount_for
method into the BookingAmount
, renaming it applied_coupon_amount
: https://gist.github.com/andrewhao/4783e3636fb1e47adfa195b749986733#file-pass_3-rb
Finally, you change your Checkout
implementation to match: https://gist.github.com/andrewhao/4783e3636fb1e47adfa195b749986733#file-pass_4-rb Here’s the new diagram: Remember: we wanted our code to match the domain language: “A Coupon is applied to a BookingAmount“. When you read the implementation in plain English, it reads:
The Checkout‘s total amount is its BookingAmount minus its applied Coupon amount.
Phew! Designing a strong Ubiquitous Language was hard work! In fact, you had spent a serious amount of time debating and clarifying with your domain experts:
Whatever you agreed on, that’s what you changed your code to reflect.
In this brief time we had together,
Domain-Driven Design is a collection of practices that illustrate how design and architecture flow from language. A strong Ubiquitous Language at the core of the project provides a stable foundation for all concepts and systems that are built upon it. Teams find themselves accomplishing their work with enhanced precision and clarity. This is just the tip of the iceberg – future posts will explore DDD architectural patterns and tools that can help you better design your systems. What experiences have you had working with a Ubiquitous Language? Share your experiences below!
Andrew is a design-minded developer who loves making applications that matter.