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 the
DriverStatusservice receives a POST we’ll need to simultaneously fetch references from the
HailingUserservice 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
/couponroute to the
checkoutAPI? We could POST to it and then it will create
Coupons in the backend. Teammate: Wait – I think we call them
Discountsin the backend. And the checkout API is technically implemented by the
RideCommerceservice. You: Oh right, I forgot. Let’s make the route
/couponand it’ll create a
Discountobject. And in this story, let’s just remember that the Checkout API really refers to the
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
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_formethod 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
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!