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.
Crossing our linguistic wires
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
DriverStatus service receives a POST we’ll need to simultaneously fetch references from the
HailingUser 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 the
checkout API? We could POST to it and then it will create
Coupons in the backend. Teammate: Wait – I think we call them
Discounts in the backend. And the checkout API is technically implemented by the
RideCommerce service. You: Oh right, I forgot. Let’s make the route
/coupon and it’ll create a
Discount object. 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.
What went wrong here?
- There was a different software system (RideCommerce) with a name that did not map cleanly back to the customer process (“checkout” flow).
- The system had two conflicting names for the same concept: Discount and Coupon.
- Extra documentation was created to note the discrepancies between concepts and names.
- This extra documentation and system knowledge did not propagate to all members of the team.
A better, Domain-Driven
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:
- Any team that builds reporting or analytics off of your software.
- Upstream data providers.
- Anybody further up the reporting chain whose purview includes the software you’re building, or its effects. Think: the Director of Finance, the COO, the head of Customer Support.
- The users of your software.
Developing a Ubiquitous Language with a Glossary
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!
Refactoring your team to use the right terms
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.
Refactoring your code to use the right 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
Here’s the current concept of how the Checkout works:
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
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:
- Is a Coupon applied to a BookingAmount, or is it added to one?
- Should we call the monetary value of a Coupon its amount, or cost?
- Is the pre-tax, pre-discount amount in the BookingAmount called a price, or a cost?
Whatever you agreed on, that’s what you changed your code to reflect.
In this brief time we had together,
- We discussed why names are important – especially in a complex endeavour like software development.
- We covered why it’s important to arrive at a shared understanding, together as a team, using the same words and vocabulary.
- We discovered how to build and integrate a Glossary into the daily rhythm of our team.
- We refactored the code twice – illustrating how to get code in line with the domain language.
And there is much more!
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!