Domain Driven Design (DDD), first described in Eric Evans’s so-called “Blue Book” is a diverse set of practices. Because these practices are loosely associated and not tied to any specific tool, it can sometimes even be unclear whether you are in fact doing DDD or something else.
There are, however, some core practices in DDD which are worth understanding and doing. The goal of DDD is to put the focus of developers on the domain by removing so-called “accidental” complexity (a key goal for any software craftsperson). Some of the DDD ideas and practices can also be viewed as bringing some of the benefits of functional programming to the object-oriented world.
The idea of ubiquitous language is simple: The code is developed to solve a problem for some people, so developers and their code should use the same terms as the people for whom they are solving the problem.
This seems like common sense, except that it’s rare in practice for institutional/organizational reasons. Developers are off in their cloister building stuff using the language of developers and architects, and they end up having to translate between what they are building and what the domain experts and end users are expecting. This is accidental complexity which increases the likelihood of and the difficulty of correcting specification problems.
DDD divides objects into several categories. Objects which fall clearly into one of these categories are easier to understand and work with in the context of a business domain…
A value object represents a piece of information in the domain which does not change but which could be replaced. For example, a bank account could contain $55. The value object is a currency object with a numeric value of 55 and a currency type value of “USD”. It could be replaced by a value of $85 if a deposit of $30 is received.
A value object can normally be immutable (which makes it easier to do functional-style processing with no confusing side effects). If a value object is replaced by another value object of the same type with identical values, no one notices or cares.
An entity has identity and state. A customer is a good example of an entity. The customer might have a customer id. Unlike value objects, entities are not interchangeable. An entity will often have “invariants” which must be enforced (do not expose any public methods, including constructors, which could allow violation of an invariant, unless it’s absolutely required by a framework you need to use). An invariant is a logical condition which applies to an entity (for example, an order always has a product ID and a customer ID and an order date, or a Flight always has a departure airport code and an arrival airport code).
An aggregate is a cluster of associated objects with a “root”, which acts as a façade – objects outside the aggregate interact only with the root. The aggregate root is responsible for assuring in each of its methods that by the end of the method call all invariants are satisfied within the aggregate (within a single transaction, generally). An example of an aggregate root can be found in this article by Julie Lerman.
A domain event encapsulates an event which could potentially trigger actions in various components of your system. The use of domain events is a kind of decoupled publish/subscribe design pattern (similar to Observer) which allows code to emit events which are handled without being responsible for how and by which components they are handled.
You already know what a module is in general. In DDD, according to Eric Evans, modules should “tell the story of the system and contain a cohesive set of concepts”, and their names should be part of the ubiquitous language and “reflect insight into the domain”.
Repositories are objects which allow retrieval of entities, aggregate roots. In DDD a repository serves as an adapter between the domain object model and the persistence model.
A complex application may have to solve different problems for different people. Sometimes the problems are similar enough to each other that you’re tempted to solve both using the same objects and paradigms. Don’t. It’s been drilled into most of us that you need to reuse objects (and database tables) as much as possible to reduce the amount of code (DRY, which is a useful practice in general), but DDD optimizes above all to minimize confusion and cognitive load rather than minimizing lines of code. A bounded context means that you solve problem A with one set of classes using one set of terms and approaches and you solve problem B with a separate set of classes using distinct terms and approaches. Even if subsystems A and B have to communicate with each other, it’s generally easier in the long run to translate between A objects and B objects for communication than to mix A objects into the B implementation or vice-versa.
Speaking of our bounded contexts A and B, there can be a core domain which is used in both context A and context B. This is called a shared kernel.
In practice, a shared kernel is a set of interfaces and objects (typically in its own library, such as a jar file in Java) which allow code outside a bound context to interact with the context. This could be service interfaces, aggregate interfaces or DTO’s (data transfer objects – generally behavior-free objects used to pass entity or aggregate data to and from a service implementation). Since the shared kernel can be used in many different places by different teams and products, it should be as stable as possible.
Supple design is a set of patterns/practices, some borrowed from functional programming, which make it easier to reason about behavior of a part of the system without worrying about what the rest of the system is doing.
- Intention-revealing interfaces
The idea is that an interface (via method names, signatures, etc.) reveals what it does, not how it’s done.
- Side-effect-free functions
If a method never changes the state of anything, other than perhaps creating one or more completely new objects, then it won’t cause deadlock or change the behavior of other methods being executed simultaneously. And if called again with the same inputs, you get the same result. It’s easy to verify with unit tests that there are no bugs.
- Standalone classes
Reduce coupling between different classes to reduce unnecessary complexity.
The idea is to enforce invariants and post-conditions of operations either in the implementation code or with unit tests (or both).
Associated design practices/architectures:
- A Hexagonal Architecture makes DDD easier because it further separates the technical infrastructure from the domain.
- CQRS with or without Event Sourcing is a complementary architecture/practice which simplifies the handling of data. It’s a big topic beyond the scope of this article, but CQRS is a useful choice for reducing hidden complexity in the spirit of DDD by separating things that do not have to go together (queries and data modification). Event sourcing is a useful technique for dealing with data that may change over time if you might ever need to know what the state was at a given moment in the past.
- Behavior-driven development (BDD) is complementary to DDD because it encapsulates the domain in tests based on the ubiquitous language.
- TDD is a good practice for pretty much any development project, and ensures that DDD assertions are validated early and often.
- Functional programming (lambdas and streams in Java, for example) works well where supple design is applied.
- Simple CRUD can coexist with DDD. Some persisted objects are simple enough that there’s no need for assertions and other domain logic.
Well, that’s a lot of interesting ideas, but how to get started using them in development projects? It’s not like a framework where there’s a tutorial that will give you a “Hello DDD” in half an hour.
Some early steps toward DDD are possible, though :
- Start using value objects. For example, you can replace numeric dollar amounts by immutable Money objects which have an amount and a currency code.
- Start using a ubiquitous language. Align the terminology used in the code and project structure with the domain language.
- Identify and segregate modules and bounded contexts. You may already have some notion of modules, but maybe you can find that you’re sharing some code between them which shouldn’t be shared.
- Get started using domain events. There are some examples on the internet that help you get started, including this C#-oriented article by Udi Dahan.
- Separate out shared kernels. The flip side of enforcing bounded contexts is sharing precisely what your module needs to share with other modules and the outside world.
- Use repositories. You may already be doing this. The key idea is to have domain code using domain storage logic (CRUD and search for entities and aggregates) rather than database logic.