Domain-driven design, or DDD, is a software design methodology aimed at producing better software. Engineers achieve this by working closely with domain experts during the continuous design process.
Eric Evans created domain-driven design and wrote a book about the practice called Domain-Driven Design: Tackling Complexity in the Heart of Software. His main points are to use models, use the language of the business, and follow specific technical patterns during development. Together, these steps will help you deal with complex software without painting yourself into a corner.
In this post, I’ll go further into demystifying domain-driven design. First, I’ll share a story about how I came to know the practice myself. Then I’ll get into some details about the modeling part of the process. Finally, I’ll do an overview of the technical bits of the craft.
How I Came to Know Domain-Driven Design
Let me be honest. When I first started hearing the term “DDD,” I would smile and nod politely. It was very mysterious to me, and I didn’t give it much thought until I got a somewhat polite comment on a TDD post I wrote. The commenter was insisting that domain-driven design was the way to go.
In that TDD post, I took the angle that test-driven development should really be “test-driven design.” Sure, there’s value in emergent design. But there’s value in other methodologies, too. So, being an open-minded, curious person, I just had to know more about domain-driven design. Soon after, I read Evans’ book and immediately saw the value in how it solves design problems.
The Design Problem
In software, there are many design problems. And, there are many tools to solve them. For one thing, we have patterns. For another, we have models. Domain-driven design uses these tools and introduces some concepts of its own for dealing with complexity.
But not so fast. What are the problems with complexity?
The Trouble With Complexity
We know intuitively what complexity is. However, it can be relative to each person and environment. A problem may be simple to one person but complex to another.
To quantify complexity, let’s think of something with more than three interconnected components and at least five business rules requiring formal definition beyond “required field.” Perhaps the problem involves some computations, data transfer, multiple screens, and an external data source. And, speaking of externalities, software is often part of a complex web of interconnected systems. How’s that for complexity? Right…it depends. To one developer, this system is complex. But another developer may eat systems like this for lunch!
Domain-driven design is all about how to deal with complexity in software development. Really, this is its value proposition. In contrast, emergent design is useful when the problem is simple. But, when you have complexity, some forethought is needed. See, when there is complexity, it will exist no matter what. You can move the complexity around but you can’t eliminate it. And the real trouble comes when you add more complexity.
On the other hand, you can contain the complexity. Domain-driven design contains complexity within “bounded contexts.”
Bounded Contexts Contain Two Things
Bounded contexts contain two things: complexity and terminology.
First, bounded contexts are meant to contain complexity. In other words, we can create a bounded context for some specific complex business logic. The bounded context connects to other contexts through adapters. And, those adapters protect against change. When those complexities contained within the context change, the rest of our system doesn’t also have to change. That’s why we should use adapters to “impedance-match” between contexts.
For another thing, bounded contexts are about defining terminology within the context. For example, the risk management department may think of employees as, well, “employees.” But the HR department may call them “personnel.” And, of course, the finance department has their own term for them: “labor resources.” Whether you love these terms or not, you’ve got to respect that the same entity has different names in different contexts.
Being a service industry, we IT folks should take the time to learn the language of our customers. When talking to the HR department, we should use the term “personnel.” But, when working with finance, we should use “labor resources,” even if that makes us cringe slightly. After all, they’re just words to us, but in the specific context, those words have meaning. Imagine how you’d feel if a non-developer used the term “unit testing” to mean “functional testing.” Could you take that person seriously if they insisted on abusing ubiquitous terminology in such a way? This is why we need to work directly with domain experts—someone working in or with intimate knowledge of the business area.
Model With the Domain Expert
Domain-driven design stresses the importance of working with the business. This is especially so when working on the model. A model is a tool for communication and planning. It can be a simple box-and-line model or a more complex format like UML. In the end, it has to be understood by both the domain expert in the business domain and the engineer(s). Modeling is an important step in communicating the business problem and iterating over solutions.
Note that this type of modeling isn’t data modeling. It’s also not database design. Data modeling and database design are exercises in defining low-level details. Those generally aren’t done in conjunction with the business. Also, this type of modeling isn’t object modeling, either. See, object models are implementation details in the same way as database design is.
The modeling in domain-driven design is about creating a model for understanding the business. It can be somewhat abstract, but it must be a shared vision. The goal is to create a joint understanding and facilitate two-way communication between the business and engineering about the business problem. Only then are we truly prepared to get into the technical details.
The Technical Parts
Once the domain is understood well enough, it’s time to build the pieces of the puzzle. Evans stresses the importance of change. As implementation happens, new understandings unfold. There may be better ways to solve certain problems. Often, you’ll need to make trips back to the modeling board. This is a good thing since those iterations over the model will further improve the design!
Design improvements aside, there are some technical recommendations in domain-driven design. Evans presents many patterns that make the software more resilient to change. A couple of patterns—the adapter pattern and bounded contexts—we’ve already discussed. Others include the factory pattern and the repository pattern. There are others, but these are the two I’ll focus on since they’re the most common. Let’s check out the factory pattern first.
Factory Pattern
When we need to create some new object, we should use the factory pattern. We don’t want every consumer to be concerned with the details of constructing an object of a specific type or sub-type. So, what do we do? We delegate the responsibility to a factory. Sure, we can call the constructor directly, but it’s best not to perform too many operations in a constructor. The solution to complex object creation is the factory! (And, by the way, a factory is really useful for containing decisions about which specific type to return.)
A factory can be a static method on the base class, or it can be its own object. The consumer must use the factory to build the object. We can specify this behavior by making the constructor for the class inaccessible to the consumer. The only way to get a new Foo is to call Foo.Build(…params). Then you’d work with the Foo object to do some Foo things:
1 2 3 4 |
Foo aFoo = Foo.build(fooData); aFoo.reportHours(7); aFoo.setManager(managerData); Integer aFooId = aFoo.save(); |
This code calls the factory method to build a Foo. Then, it uses the object to report hours and set the manager. Finally, it saves and gets the id generated from the save. Later, we can use the id to rehydrate a Foo from the saved data.
Rehydrate Using a Repository
A repository is an abstraction of a persistence mechanism. A few common persistence mechanisms are a database, a file, and memory. Basically, the repository represents anything that can save and retrieve the data for the object. All of this happens inside the object or the factory. In this way, a domain class is dependent on the repository.
A domain class is something that represents a real-world concept. It can also be called a domain object. These domain classes are what your application works with. So, how do we rehydrate a domain object using a repository?
Luckily, all that can happen in the factory so it’s all nicely contained. Here’s a quick look at how you might accomplish this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Foo aFoo = Foo.Get(aFooId); //---// public class Foo { private Foo(FooData data) { ... } public static Foo Build(FooData data) { return new Foo(data); } public static Foo Get(int id) { var data = ServiceLocator.GetRepository<Foo>().Get(id); if(!data) throw new NoFooFoundException(id); return new Foo(data); } } |
You can see that Foo.Get uses a ServiceLocator to get the repository, but that’s an implementation detail. You can accomplish the same goal using dependency injection.
You’ve got to remember that domain-driven design came around more than a decade ago. Since then, our tools have evolved. Even so, there are many important concepts here. One really good takeaway is containing all the code that’s primarily responsible for making a Foo. These are important object-oriented programming principles that get cast aside too frequently. When these patterns are missing, a codebase becomes much more difficult to maintain!
In the End, Domain-Driven Design Is About Organization
All the terminology and technical practices add up to one thing in the end—organization. Evans’ book carries the subtitle “Tackling Complexity in the Heart of Software” for good reason. Complex software requires special care to advance on the good road ahead. Without it, the project can get bogged down in the mud. The concepts and practices of DDD are a well-thought-out way to manage the complexity. There are others, to be sure, but this one has enough merit that you should pay attention to it! If you haven’t read the book yet, I highly recommend grabbing a copy and checking it out.
I loved that you started with the domain modeling. I found Bounded Contexts to be the most valuable contribution of DDD. I’ve been using Event Storming lately to do modeling with domain experts. I found it enables to come up with a Rough Design Up Front in just a few days of workshop. It’s a great addition to evolutionary design. I’m currently writing about this on my blog https://philippe.bourgau.net/misadventures-with-big-design-up-front/
Thanks again for your post.
Thanks! What’s really cool about Bounded Contexts is that it creates a good foundation for microservices design.