Domain-driven design (DDD), a software development technique first proposed by Eric Evans, includes strategic, philosophical, tactical, and technical elements and is related to many specific practices. I've written about why you need DDD (even though you think you don't), but the question after deciding to use it is—how do I learn DDD?
While the full explanation would take a couple of 500-page books, the essence of DDD is profoundly simple: capture the domain model in domain terms, embed the model in the code, and protect it from corruption. We can understand these concepts and put them to good use right away. While this level of understanding is somewhat shallow, it is still useful—and it might be refreshing enough to take the plunge. (Note: In the examples below, I’ve assumed an object-oriented project, but the principles still apply in other cases.)
DDD strategy and philosophy
You probably know that DDD has strategic value; that’s why so many companies with extremely complex domains rely on it to produce software that can rapidly evolve with the business. But did you know that DDD also has a philosophical theme? You might have heard the term “ubiquitous language,” which is a mouthful when you’re speaking about it, but it’s a shorthand way of emphasizing the fundamental principle of DDD: Use domain terminology everywhere; make it ubiquitous. When practicing DDD, this basic philosophy of the primacy of domain terminology can be distributed across three guiding principles:
- Capture the domain model, in domain terms, through interactions with domain experts. In other words, talk to the people in the businesses where you are solving problems and understand them from their point of view first and foremost. This is how you form the ubiquitous language of the domain and set the foundation for harmonious models.
- Embed the domain terminology in the code. This means naming things the way the domain expert would name them, including classes, methods, commands, and especially domain events. This is how you reflect the domain model in the code.
- Protect the domain knowledge from corruption by other domains, technical subdomains, etc. If you find that your code is talking about two different things—e.g., the domain solution and the technical implementation—separate those components to keep the subdomains apart. This strategy tends to result in classes with single responsibilities and a terse, focused vocabulary. Put "translators" at the boundaries between subdomains to keep them from depending on each other’s structures unnecessarily, and also to prevent blurring of the meaning of domain terms.
These three principles guide and inform DDD. Knowing and using them provides benefits, even without the rest of the DDD practices and patterns. Let’s look at how we might reap some benefits from just using this information in our software development projects
Understanding the domain and building a ubiquitous language (UL)
The first element, capture the domain model in domain terms, underlies the notion of the ubiquitous language, which is a common set of terms and definitions used by the entire team, both technical and nontechnical. This language is about the business domain and uses terminology from the business domain—not IT technobabble terms.
Understanding the business problem in business terms allows the developers to communicate more clearly with the business stakeholders and among the technical team. Finally, when everyone is using the same terminology and telling the same stories, the entire team has a shared model of the problems that can guide solutions toward models that reflect how the business operates, instead of how the software operates.
When talking with domain experts, first do your homework so you can ask leading questions. Use real-world scenarios to drive understanding of the domain and simple models to capture the basics, and note the problems and pain points as you go. Have the domain experts explain the operations and problems to you, and explain it all back using the same terminology. Walk through proposed solutions the same way, refining your mutual understanding of the terms and issues as you go while evolving the common language and identifying relevant subdomains.
Modeling is design
It’s all design, at some level. The difference is that some models are low fidelity and intended to merely be reminders of the larger discussion, while other models are detailed and formalized enough to be executed directly. Yes, code is another kind of modeling language; it just happens to be executable. Even simple event modeling can be instructive for everyone involved and sufficient to identify critical features and operations. Yet in all levels of design, the UL must remain prominent.
If you find yourself modeling computer-y things, stop. You’ve gone too deep, unless you’re figuring out some technical knot in a subdomain. In general, keep the models at the level of things that the domain expert would find interesting. Validate the models with prototypes and spikes as soon as possible, to more rapidly learn what works and what doesn’t.
Intention-revealing interfaces
When you begin to embed the domain model in your code, it’s kind of obvious that you should name classes the same as their real-world counterparts, but it’s not so obvious that you should also name methods the same as their domain-level operations.
For example, if the domain expert says that at a certain point in the application the user can sign up for a subscription to a magazine, please don’t name the method that implements that functionality “CreateCustomer.” Instead, name it as specifically as you can, in domain terms. Imagine that the domain expert is going to look at your code one day. Would she understand the gist of it from the interface, i.e. from the class and method names? Instead of “CreateCustomer,” perhaps "SignUpForMagazineSubscription" would be a better name.
In this way, your code naturally builds up a set of classes and interfaces that reflect the domain concepts. This is how you achieve harmonious models, by using the same terminology in conversations, models, and code. The result is a task-oriented user interface that matches the mental model of the user, or an API that makes sense to new developers familiar with the domain.
Context mapping
Certain concepts in the domain discussion are going to be key. They will have activities centered on them and may define some common terms differently than other areas. In other words, islands will begin to form in the models and conversations around bounded contexts, which are subsets of the code that have their own, independent model, with a specific interface for use by other parts of the code.
The typical pattern for a context boundary is a set of commands accepted externally, with data/object transformations at the boundary from “outside” to “inside.” All external knowledge and dependencies are kept at bay by this boundary, allowing the code inside the boundary to use only its own subdomain-specific classes, interfaces, and terms. This supports independent development streams, with communication via well-defined interfaces.
This also supports one more activity: context mapping. This consists of identifying and classifying the relationships between bounded contexts within the domain.
Suppose you have two bounded contexts, one responsible for maintaining the catalog of available magazine subscriptions and another for processing credit-card charges. The relationship between these two contexts matters. They may share some common concepts and have different levels of trust during information exchanges.
This comes into play during development, manifesting as the relationship between the two teams. If the team working on the credit-card charges depends directly on the magazine class used by the catalog team, then we say that the credit-card charges team has a "consumer" relationship with the catalog team. If the catalog team has to change the magazine class, this will trigger a change in the credit-card context.
Similarly, if the credit-card team needs additional information added to the magazine class to complete a feature, but the catalog team won’t make the change or can’t get to it for four weeks, that will affect implementation planning in significant ways. This is often referred to as the "bandwidth" between the teams. Note that dependencies may be mutual. In the worst case, every context may rely on the same “shared library,” which, if it's not stable, can disrupt development in every context.
The information discovered in context mapping lets you mitigate and rearrange dependencies between contexts by adjusting the "knobs," to allow for maximally independent development. Note that in some cases, team/context boundaries may be dictated by business constraints, not logical/software constraints. Context mapping can reveal these issues as well.
Dipping your toes in the water
Now that you’ve splashed around in the shallow end of the DDD pool, do you see ways that it might help your software development efforts? Even the simplest things, such as always using the same name for the same things in the same context, can prevent serious confusions among team members, make the code easier to understand, and improve communication with nontechnical stakeholders. So go forth and do a little DDD, and see what happens.
Keep learning
Take a deep dive into the state of quality with TechBeacon's Guide. Plus: Download the free World Quality Report 2022-23.
Put performance engineering into practice with these top 10 performance engineering techniques that work.
Find to tools you need with TechBeacon's Buyer's Guide for Selecting Software Test Automation Tools.
Discover best practices for reducing software defects with TechBeacon's Guide.
- Take your testing career to the next level. TechBeacon's Careers Topic Center provides expert advice to prepare you for your next move.