An introduction to Domain-Driven Design
Domain-Driven Design is an approach to software development based on making your software deeply reflect a real-world system or process.
“Domain” in Domain-Driven Design officially refers to a “sphere of knowledge and activity around which the application logic revolves”. In other words, the “Domain” is what is commonly referred to as “business logic” in the software world.
In Domain-Driven Design, business logic is considered to be the heart of the software.
You’ll find in this article an introduction to Domain-Driven Design that mostly follows what is explained in Eric Evans’ book. Other implementations and vocabulary also exist, as well as similar architectures sharing the same principles, such as clean architecture and hexagonal architecture.
Is Domain-Driven Design for me?
The goal of Domain-Driven Design is to free up the domain code from the technical details, to have more space to deal with its complexity. It is a good fit for dealing with highly complex domains, and projects beginning to dip into legacy.
Going for a Domain-Driven approach also means higher costs at first. Developers will first face a steep learning curve and managing the architecture will make things longer to build. For these reasons, Domain-Driven Design is not recommended for simple projects and unexperimented teams.
At Inato, Domain-Driven Design was a good match because we were beginning to find our code hard to test, hard to read from a functional perspective, and hard to extend when new use cases were coming up. The complexity of the product was going up quickly and we needed to make our code base architecture scale to handle it and ship value faster.
First, an example
Before diving into concepts, here is a small example of how Domain-Driven code looks. This example implements an update to a shopping cart. I encourage you to go back to it after reading the rest of the article.
You can already see here that:
- The code is very expressive, because the code can be read almost like plain English.
- The code is easy to test (See how we would write a test here).
- Concerns are separated, because this business code is independent from the way data is stored.
The basics of Domain-Driven Design
We’ll cover three concepts that make most of what Domain-Driven Design is.
- Separating the concerns into layers
- Modeling the Domain
- Managing the life-cycle of Domain objects
🍰 I. Isolating the domain: the layered architecture
Domain-Driven Design focuses on domain modeling, and separating the model (or business logic) from the implementation details (e.g. which database we use).
Indeed, if the domain-related code is mixed with other code, it becomes rapidly very difficult to reason about.
The recommended architecture is made of 4 layers.
User Interface (or Presentation Layer)
Responsible for showing information to the user and interpreting the user’s commands. The external actor might sometimes be another computer system rather than a human user.
Defines the jobs (use cases) the software is supposed to do and coordinates the domain objects to work out problems.
This layer is kept thin. It does not contain business rules or knowledge, but only coordinates tasks and delegates work to collaborations of domain objects in the next layer down.
It does not have state reflecting the business situation, but it can have state that reflects the progress of a task for the user or the program.
Domain Layer (or Model Layer)
Responsible for representing concepts of the business, information about the business situation, and business rules.
State that reflects the business situation is controlled and used here, even though the technical details of storing it are delegated to the infrastructure.
This layer is the heart of business software.
Provides generic technical capabilities that support the higher layers: message sending for the application, persistence for the domain, drawing widgets for the UI, and so on. The infrastructure layer may also support the pattern of interactions between the four layers through an architectural framework.
🌐 II. Domain modeling
Domain modeling is the activity of describing the domain knowledge with concepts and structures that will help reason about the domain and implement it.
The basic constraint is that the model must both help the implementation of features and represent real-life knowledge.
To enforce the representation of the domain inside the code, Domain-Driven Design also encourages the use of an “Ubiquitous Language”, which is shared between developers and business people.
👩🏫 1. Best practices to enforce healthy domain modeling
- Use the model as the backbone of the Ubiquitous Language.
- Commit the team to exercising that language relentlessly in all communication within the team and in the code.
- Use the same language in diagrams, writing, and especially speech.
- Resolve confusion over terms in conversation, in just the way we come to agree on the meaning of ordinary words.
- When changes are made to the model, refactor the code (renaming classes, methods, modules, …) to conform to the new model.
- Recognize that a change in the Ubiquitous Language is a also change to the model.
- Conversely, developers need to realize that changing the code also means changing the model.
- Domain experts (product people) should object to terms or structures that are awkward or inadequate to convey domain understanding; developers should watch for ambiguity or inconsistency that will trip up design.
🏗 2. Expressing the model: Building Blocks
There are 3 tools to express the model in Domain-Driven Design, which can be grouped in Modules:
- Value Objects
Value Objects are simple objects that convey meaning and functionality. These objects describe things but don’t have a special identity.
Value Objects are often passed as parameters in messages between objects. They are frequently temporary created for an operation and then discarded.
- Treat the Value Object as immutable.
- Don’t give it any identity and avoid the design complexities necessary to maintain Entities.
- Ensure that the attributes that make up a Value Object form a conceptual whole.
An Entity is an object defined primarily by its identity, rather than specific attributes. The identity of an Entity runs through time, and possibly different representations. Entities are also called “reference objects”.
In some cases, the clearest and most pragmatic design includes operations that do not conceptually belong to any object. Rather than force the issue, we can follow the natural contours of the problem space and include Services explicitly in the model.
The Modules in the domain layer should emerge as a meaningful part of the model, telling the story of the domain on a larger scale.
There should be low coupling between Modules and high cohesion within them, both code-wise and concept-wise:
- There is a limit to how many things a person can think about at once (hence low coupling).
- Incoherent fragments of ideas are as hard to understand as an undifferentiated soup of ideas (hence high cohesion).
- Give the Modules names that become part of the Ubiquitous Language. Modules and their names should reflect insight into the domain.
- When creating modules, favor conceptual clarity over technical convenience (if both are not achievable together).
🗃 III. Managing the life cycle of domain objects
The goal is to prevent the model from getting swamped by the complexity of managing the life cycle. To do this, we separate the management of the life cycle (i.e. persisting objects) from the business logic.
The most important concepts for this are Aggregates and Repositories. Note: an Aggregate is always associated with one and only one Repository.
Aggregates are a cluster of Entities and Value Objects that make sense domain-wise and are retrieved and persisted together.
Aggregates add structure to the model by setting boundaries and providing a clear ownership for the objects they contain.
- Cluster the Entities and Value Objects into Aggregates and define boundaries around each.
- Choose one Entity to be the root of each Aggregate, and control all access to the objects inside the boundary through the root.
- Allow external objects to hold references to the root only. This arrangement makes it practical to enforce all invariants for objects in the Aggregate and for the Aggregate as a whole in any state change.
⚠️ Even though aggregates help managing the life cycle by defining ownership and boundaries, they know nothing about the details of the infrastructure and belong in the domain layer.
Repository offer an interface to retrieve and persist aggregates. They hide the database details from the domain. Repositories manage the middle and end of the life cycle.
Repository interfaces are declared in the Domain Layer, but the repositories themselves are implemented in the Infrastructure Layer. This makes it easy to switch between different implementations of a repository without impacting any business code (for instance going from SQL to No-SQL storage, or writing in-memory implementations for faster tests).
Don’t forget to check back on the code example from the beginning to see how all of it combined in real code.
There’s actually much more to it than this, so I really recommend to read one or two books before going at it in a production code base. Here are the best resources we have used so far at Inato to help us going with Domain-Driven Design:
- The book Domain-Driven Design by Eric J. Evans
- The book Implementing Domain-Driven Design by Vaughn Vernon
- A repository implementing the example from the book Domain-Driven Design: https://github.com/citerus/dddsample-core/tree/master/src/main/java/se/citerus/dddsample