The last time we talked about software architecture, I introduced you to Layered Architecture. Today we will start with Clean architecture (also called Onion or Ports and adapters architecture) and more specifically domain-driven design.
Clean architecture as a concept is significantly more complex than layered architecture. So I decided to split the article into two parts. In today’s article, I will try to explain the idea behind Domain-driven design simply.
What is Domain-driven design?
We need to understand domain-driven design because it lies in the center of Clean architecture (figuratively and literally).
Domain-driven design has its technicalities (that we are going to inspect in detail), but in the first place, DDD is a way of thinking.
As we can see from the name, this design approach is driven by the business domain. Or, said another way, we focus entirely on the business logic of the domain.
Integration and infrastructure features we consider details that the domain model does not know nor depend on.
In turn, the business domain can have sub-domains, and these sub-domains are isolated from each other as much as possible.
Let’s provide a simple example. Suppose we have to create an application for a food delivery system using domain-driven design.
The domain here is the entire process of ordering, payment, communication with the restaurant, and delivery and tracking.
And the sub-domains, as we can see, are order management, delivery management, etc.
Using DDD toward the example above should create an encapsulated core consisting of clearly defined business entities with strict boundaries. Don’t get panic. We will explain everything.
Domain-driven design first steps
The hardest thing about domain-driven design is building the right logical model of the domain and its sub-domains and defining proper boundaries. That is something that the developers can’t and should not do alone.
Developers should work in very close collaboration with the business people. Only business people know how exactly they operate their business, and only they can clarify the domain and subdomains to the developers.
At this point, developers should not take into consideration any technicalities, any. Their mission is to model the domain and its sub-domains only through the business logic point of view. They don’t think about databases, persistence, representation, other integrations, etc. The models they create are pure objects limited to any external dependencies.
Developers and business people should create a common language for business objects in the system. This common language is called ubiquitous language. Behind that fancy word hides the simple goal for common understanding. Terms (this also applies to the code) should be named so anybody can understand them.
Before continue with the main domain-driven design elements, let’s clarify some terms.
Bounded context is a pattern whose main idea is to draw boundaries between different sub-domains. We do this because we want each sub-domain to be as independent as possible.
From the example above, we see that our food delivery system has different sub-domains. Two of these sub-domains are order management and delivery management. The models that represent the two sub-domains should be isolated. Order management models shouldn’t know anything about the delivery models and vice versa.
If we have a model that should be represented in the two sub-domains, we duplicate that model. For example – both sub-domains need a customer as a model. When we copy the model, we extract only the required properties for the specific sub-domain. So it is normal to have a model with the same name but with different properties in different sub-domains (of-course, we track that model by uniform ID in both sub-domains).
Anemic vs. rich models
For domain models, there are two concepts. They are anemic and rich model implementation.
Anemic models are models with only properties without any logic. Often they are also badly encapsulated using just public setters and getters. For example, anemic models we use when we have a database-centric architecture (like layered architecture).
Rich models have business logic. They are very well encapsulated. Instead of public setters and getters, we use methods for all the specific operations. Rich model collections are read-only and can only be modified through the methods and the constructor. Rich models can not be created nor exist in an invalid state. The implementation should guarantee that after any operation, the model is in a valid state.
In domain-driven design, as you might guess, we use rich models.
Domain-driven design elements
Using the domain-driven design, we will face several different elements that build the core of it. They are:
- Value object
- Aggregate (and Aggregate root)
- Domain event
An entity in domain-driven design is a rich model element that has a unique identification. For example, we might have an Order entity. The Order entity has an identifier (like autoincrement bigint). Using that identification, we track the entity and build relations.
Value objects are model elements that do not have a specific identification. Their identification is based on the comparison (by value) of all their attributes.
They also can contain business logic. Value objects mainly live as attributes of other entities.
A good example of a value object is the delivery address of the Order entity.
Aggregate is a very specific structure for the domain-driven design.
An aggregate is a group of objects that should be handled together. Every aggregate has an aggregate root. The aggregate root is the main object responsible for the control of the other objects.
Any command or request toward the elements in the aggregate should come through the aggregate root.
In our example, with the food delivery system, such an aggregate is the grouping of Order and OrderItem entities. In this case, the Order entity is the aggregate root. If we want to add a new OrderItem to the Order, we should do it through the aggregate root.
Entities that are “below” the root level should not allow state modification outside the aggregate.
Using aggregates, we strive to achieve an always valid business model.
In domain-driven design, as we said, the models should be encapsulated and isolated as much as possible.
For example, the Order aggregate should not reference or be referenced by the Delivery aggregate.
They not only should not know about each other but also contract-based injection is forbidden.
The only right way of communication is through domain events (some of you may argue, but I firmly believe that).
Every aggregate can publish and subscribe to events.
If a new order is created, the Order aggregate publishes OrderCreatedEvent event. Every other aggregate that is interested in this event can handle it and process it.
Domain events are part of the domain model, but their artifacts are processed outside the domain layer. In the next article, we will see that in detail.
As most of you know, a factory is a design pattern for creating objects. In domain-driven design, the factory pattern isolates the creation of domain aggregates from the application layer.
In the best-case scenario, a factory should receive plain DTO (or just primitive types) and contains as little as possible logic.
Factory pattern should always create objects in a valid state.
The Repository is another well know pattern. The idea behind it is to encapsulate communication to the data storage and provide a layer of abstraction.
In domain-driven design, each aggregate root has a repository. But as we said, the domain model should not be familiar with any infrastructure mechanisms. That’s why we don’t put repository implementations in the domain model but the infrastructure layer.
In the domain model, we put only the contracts (interfaces) of the repositories. That also will be discussed in detail in the next article.
Domain-driven design in short
Domain-driven design is a way of thinking.
Working very closely with the business people, we aim to model the different domains of the business.
We create models (entities, value objects, aggregates) using only simple classes and primitive types. We don’t care about persistence, UI, external integrations, external libraries, etc. These things don’t belong in our domain core.
Models have complex logic, but we strictly encapsulate that behavior. They are always in a valid state, even after their initial creation.
Often we group different models because they can not live in a valid state only by themselves. Those groups we call aggregates.
Models live in isolation, so they can not communicate directly. For that reason, we use domain events.
The domain core does not depend on anything. Other layers (like application and infrastructure) depend on it.
What is next?
As I said above, domain-driven design is a core concept of Clean architecture, and that’s why it is essential to get comfortable with that way of thinking. It is entirely normal to be confused at first, but this should not discourage you.
As a next step, I suggest you read Eric Evans’s excellent book “Domain-Driven Design: Tackling Complexity in the Heart of Software”. While reading it, try to apply the knowledge to systems you have already developed without DDD. Believe me, that exercise will help you a lot.
In the next article, we will see how the domain model fits in the Clean architecture structure and the point behind all this complexity.