Following part 1 about DDD, in this part 2 I would like to introduce to you the commonly used architectures with DDD as well as other concepts surrounding the domain layer in DDD, hope you will receive it warmly.
Architectures commonly used with DDD
When applying the idea of DDD to system design, we will often encounter the following common architectural styles:
- 3-layer architecture (3 layers architecture).
- Layered architecture.
- Onion architecture.
- Hexagonal architecture (Hexagonal architecture – Port and Adapter architecture).
- Clean architecture.
We will go through the above architectural types in turn as overview and summary as possible.
3-layer architecture (3 layers architecture)
Outline of its shape:
This is a common architecture used by many web systems, in addition to being popular, its implementation is not too difficult, but it has major disadvantages as follows:
- The layers will be very binding and interdependent, making it difficult to maintain or refresh each floor.
- Linkage to a business logic class is very low.
To illustrate the above, let’s look at the following example:
Suppose we have a task management system with the following basic functions:
- User cannot register multiple emails at the same time.
- Newly created User will have a status of
USING
but can also be converted toSUSPENDING
. - Tasks can only be assigned to users whose status is
USING
. - Task has only 2 states,
COMPLETE
orDOING
.
Its use-case diagram will look like this:
Obviously, we see that the 2 domain knowledges knowledge User
and Task
are completely unrelated, so writing them in the same business logic layer
will reduce the connection.
Briefly about association , this is a metric to evaluate the “quality” of a class. We take the following example:
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">class</span> <span class="token class-name">OperationUntil</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> counter <span class="token operator">:</span> <span class="token builtin">number</span> <span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token function">increment</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span> counter <span class="token operator">++</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token function">greet</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span> <span class="token builtin">console</span> <span class="token punctuation">.</span> <span class="token function">log</span> <span class="token punctuation">(</span> <span class="token string">"Hello world"</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
We see that the greet
method has absolutely nothing to do with the counter
property of the class, moreover, the name of the class OperationUntil
is also a very generic name that is not clear in meaning. Therefore method
and property - thuộc tính
of the class have NO RELATIONSHIP to each other. So this OperationUntil
class can be considered as low coherence.
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">class</span> <span class="token class-name">Counter</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> count <span class="token operator">:</span> <span class="token builtin">number</span> <span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token function">increment</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span> count <span class="token operator">++</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token function">getCurrentCount</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token builtin">number</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> count <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
getCurrentCount
and count
are now related to each other and also to the Counter
class, so we can conclude that this class is highly interconnected.
Going back to User
and Task
example above, it is completely unreasonable for us to implement domain logic at the Data Access layer because this layer will take on two tasks:
- Implement business logic (model)
- Perform DB (table) related processing
So it will make the two layers business logic
and data access
have a certain bond with each other.
ràng buộc
here shows the interdependence between layers, classes
Let’s take for example:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token keyword">class</span> <span class="token class-name">Counter</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">static</span> count <span class="token operator">:</span> <span class="token builtin">number</span> <span class="token operator">=</span> <span class="token number">0</span> <span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token function">increment</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span> count <span class="token operator">++</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">class</span> <span class="token class-name">Printer</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token function">print</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span> <span class="token builtin">console</span> <span class="token punctuation">.</span> <span class="token function">log</span> <span class="token punctuation">(</span> Counter <span class="token punctuation">.</span> count <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Obviously, the print method of the Printer class depends on the count property of the Counter class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token keyword">class</span> <span class="token class-name">Counter</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">static</span> count <span class="token operator">:</span> <span class="token builtin">number</span> <span class="token operator">=</span> <span class="token number">0</span> <span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token function">increment</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span> count <span class="token operator">++</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">class</span> <span class="token class-name">Printer</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token function">print</span> <span class="token punctuation">(</span> input <span class="token operator">:</span> <span class="token builtin">number</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token keyword">void</span> <span class="token punctuation">{</span> <span class="token builtin">console</span> <span class="token punctuation">.</span> <span class="token function">log</span> <span class="token punctuation">(</span> input <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> Printer <span class="token punctuation">.</span> <span class="token function">print</span> <span class="token punctuation">(</span> Counter <span class="token punctuation">.</span> count <span class="token punctuation">)</span> <span class="token punctuation">;</span> |
We see that passing parameters to the print method of the Printer class reduces the interdependence of the Printer and Counter classes.
Layered architecture
In this architecture, we will separate the Bussiness Logic Layer
Layer into 2 layers:
Application Layer
– implement usecaseDomain Layer
– implement domain logic
In this architecture:
- The connection between floors was higher.
- The dependency between layers has been reduced but the domain layer still depends on the infra layer (depending on using DB or OR Mapper) and this is something to avoid
Onion architecture
It is similar to the layered architecture above, but the dependence of the domain layer on the infra layer has been completely eliminated
Specifically, the domain layer will define the Repository Interface
, the infra layer will “implement” the above-mentioned interfaces. So now the infra layer will depend on the domain layer. The infra layer will also save Domain Aggregate
into the DB.
The characteristics of the remaining layers (Presentation, Usecase, Domain) will be as follows:
- Domain layer: need to be independent, not dependent on any layer. Contains
Value-Object
,Domain Aggregate
,Domain Event
- Usecase layer: use public methods of
Domain Aggregate
, this layer is not allowed to depend on presentation layer - Presentation layer: interacts directly with the client, it contains classes:
Controller
,External-Controller
The access of external Actors to the Application as well as receiving req from the Application is through the Adapters.
Hexagonal architecture – Port and Adapter architecture
The main idea here is that the Application will interact with the outside world through:
- Adapter
- Dedicated port
The summary of this architecture will be as follows:
Clean architecture
This architecture is the synthesis and inheritance from Onion Architecture
and Hexagonal Architecture
.
The naming of each floor may be slightly different:
- Usecase Layer → Application layer
- Adapter Layer → Interface Adapter Layer
Concepts around the domain layer
In addition to the concept of domain aggregate as mentioned in the previous section, we also have other concepts at the domain layer such as:
- Value-Object
- Domain Services
- Repository
- Factory
Value Object
Value-Object is a value type that makes no sense when not tied to any particular Aggregate. Let’s take for example:
In the company, each employee will have his own employee number (eg 1234), and this number will only be associated with a single employee. This code will be used to:
- Work progress management
- Human resources management
- …
This means that the number 1234 has a very important meaning for the company and the employee itself. However, if it is not associated with any employee, it is simply a number , and it does not represent any business logic.
In my API I have defined a class EmailValueObject
as follows: https://github.com/tuananhhedspibk/NewAnigram-BE-DDD-Public/blob/main/src/domain/value-object/email-vo.ts
With regular email, just following the correct format xxx@domain is enough, but maybe due to the specifics of the system, you can add other conditions for the email (eg: must start with a lowercase letter, .. .), the above conditions are business logic belonging to the domain layer.
In my API, I specify that the email must conform to a format checked by the predefined regex as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token keyword">const</span> mailRegex <span class="token operator">=</span> <span class="token regex"><span class="token regex-delimiter">/</span> <span class="token regex-source language-regex">^(([^<>()[]\.,;:s@"]+(.[^<>()[]\.,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$</span> <span class="token regex-delimiter">/</span></span> <span class="token punctuation">;</span> <span class="token keyword">const</span> mailSchema <span class="token operator">=</span> z <span class="token punctuation">.</span> <span class="token function">string</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">regex</span> <span class="token punctuation">(</span> mailRegex <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> mailSchema <span class="token punctuation">.</span> <span class="token function">parse</span> <span class="token punctuation">(</span> input <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">this</span> <span class="token punctuation">.</span> value <span class="token operator">=</span> input <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span> e <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">DomainError</span> <span class="token punctuation">(</span> <span class="token punctuation">{</span> code <span class="token operator">:</span> DomainErrorCode <span class="token punctuation">.</span> <span class="token constant">BAD_REQUEST</span> <span class="token punctuation">,</span> message <span class="token operator">:</span> <span class="token string">'Invalid email format'</span> <span class="token punctuation">,</span> info <span class="token operator">:</span> <span class="token punctuation">{</span> detailCode <span class="token operator">:</span> DomainErrorDetailCode <span class="token punctuation">.</span> <span class="token constant">INVALID_EMAIL_FORMAT</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Domain Services
Used when representing the model with an object is not possible . Usually will work with a set of objects .
For example, a typical example is checking whether mail is duplicated or not – in other words, mail has been used for users in the system or not. A user object itself can know its own mail but cannot know information about another object’s mail, so it is impossible to check it yourself.
Such cases will usually be handled by domain service
.
But:
Try to use entity and value-object as much as possible and minimize the use of domain-service
The reason is because if you accidentally write a lot of business logic here, in the future it will become an unwanted Fat class.
Repository
Used to save Aggregate
‘s data to the DB. Usually 1 aggregate corresponds to a Repository.
Passing the aggregate to the repository or returning the aggregate must go through root aggregate
. In my API, I create UserRepository
as follows:
First, I define an abstract class IUserRepository
at the domain layer.
1 2 3 4 5 6 7 8 | <span class="token keyword">export</span> <span class="token keyword">abstract</span> <span class="token keyword">class</span> <span class="token class-name">IUserRepository</span> <span class="token punctuation">{</span> <span class="token function-variable function">getByEmail</span> <span class="token operator">:</span> <span class="token punctuation">(</span> transaction <span class="token operator">:</span> TransactionType <span class="token operator">|</span> <span class="token keyword">null</span> <span class="token punctuation">,</span> email <span class="token operator">:</span> <span class="token builtin">string</span> <span class="token punctuation">,</span> <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token builtin">Promise</span> <span class="token operator"><</span> UserEntity <span class="token operator">|</span> <span class="token keyword">null</span> <span class="token operator">></span> <span class="token punctuation">;</span> <span class="token function-variable function">save</span> <span class="token operator">:</span> <span class="token punctuation">(</span> transaction <span class="token operator">:</span> TransactionType <span class="token punctuation">,</span> user <span class="token operator">:</span> UserEntity <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token builtin">Promise</span> <span class="token operator"><</span> UserEntity <span class="token operator">></span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> |
It defines two method signatures, getByEmail
& save
, with functions respectively:
getByEmail
: get user from DB via email.save
: save the user’s data to the DB.
I implement this abstract class at the infra layer as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <span class="token keyword">class</span> <span class="token class-name">UserRepository</span> <span class="token keyword">implements</span> <span class="token class-name">IUserRepository</span> <span class="token punctuation">{</span> <span class="token keyword">async</span> <span class="token function">getByEmail</span> <span class="token punctuation">(</span> transaction <span class="token operator">:</span> Transaction <span class="token operator">|</span> <span class="token keyword">null</span> <span class="token punctuation">,</span> email <span class="token operator">:</span> <span class="token builtin">string</span> <span class="token punctuation">,</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token builtin">Promise</span> <span class="token operator"><</span> DomainUserEntity <span class="token operator">|</span> <span class="token keyword">null</span> <span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">const</span> repository <span class="token operator">=</span> transaction <span class="token operator">?</span> transaction <span class="token punctuation">.</span> <span class="token function">getRepository</span> <span class="token punctuation">(</span> RDBUserEntity <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token function">getRepository</span> <span class="token punctuation">(</span> RDBUserEntity <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token keyword">this</span> <span class="token punctuation">.</span> <span class="token function">getBaseQuery</span> <span class="token punctuation">(</span> repository <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> user <span class="token operator">=</span> <span class="token keyword">await</span> query <span class="token punctuation">.</span> <span class="token function">where</span> <span class="token punctuation">(</span> <span class="token string">'user.email = :email'</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> email <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">getOne</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">return</span> userFactory <span class="token punctuation">.</span> <span class="token function">createUserEntity</span> <span class="token punctuation">(</span> user <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">async</span> <span class="token function">save</span> <span class="token punctuation">(</span> transaction <span class="token operator">:</span> TransactionType <span class="token punctuation">,</span> user <span class="token operator">:</span> DomainUserEntity <span class="token punctuation">,</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token builtin">Promise</span> <span class="token operator"><</span> DomainUserEntity <span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">const</span> repository <span class="token operator">=</span> transaction <span class="token operator">?</span> transaction <span class="token punctuation">.</span> <span class="token function">getRepository</span> <span class="token punctuation">(</span> RDBUserEntity <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token function">getRepository</span> <span class="token punctuation">(</span> RDBUserEntity <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> salt <span class="token operator">=</span> <span class="token function">randomlyGenerateSalt</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> passwordHashedWithSalt <span class="token operator">=</span> <span class="token function">hashPassword</span> <span class="token punctuation">(</span> user <span class="token punctuation">.</span> password <span class="token punctuation">.</span> <span class="token function">toString</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">,</span> salt <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> createdUser <span class="token operator">=</span> <span class="token keyword">await</span> repository <span class="token punctuation">.</span> <span class="token function">save</span> <span class="token punctuation">(</span> <span class="token punctuation">{</span> email <span class="token operator">:</span> user <span class="token punctuation">.</span> email <span class="token punctuation">.</span> <span class="token function">toString</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">,</span> password <span class="token operator">:</span> passwordHashedWithSalt <span class="token punctuation">,</span> userName <span class="token operator">:</span> user <span class="token punctuation">.</span> userName <span class="token punctuation">,</span> salt <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">return</span> userFactory <span class="token punctuation">.</span> <span class="token function">createUserEntity</span> <span class="token punctuation">(</span> createdUser <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
You can refer to the full source code of abstract class
here and implement class
here .
The reason I use abstract class and not interface here is: I want IUserRepository
at the domain level to be like a BaseClass
of the domain user so that I can create more SubBaseClass
of other domain users like IAdminRepository
or IMemberRepository
.
Factory
Often used to create new objects when the logic of creating objects is quite complex. The factory itself is also considered a kind of domain service
.
I take an example with the API I developed as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <span class="token keyword">import</span> <span class="token punctuation">{</span> plainToClass <span class="token punctuation">,</span> ClassConstructor <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@nestjs/class-transformer'</span> <span class="token punctuation">;</span> <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">UserFactory</span> <span class="token punctuation">{</span> <span class="token keyword">protected</span> <span class="token generic-function"><span class="token function">createEntity</span> <span class="token generic class-name"><span class="token operator"><</span> <span class="token constant">E</span> <span class="token punctuation">,</span> <span class="token constant">P</span> <span class="token operator">></span></span></span> <span class="token punctuation">(</span> entity <span class="token operator">:</span> ClassConstructor <span class="token operator"><</span> <span class="token constant">E</span> <span class="token operator">></span> <span class="token punctuation">,</span> plain <span class="token operator">:</span> <span class="token constant">P</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token constant">E</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">plainToClass</span> <span class="token punctuation">(</span> entity <span class="token punctuation">,</span> plain <span class="token punctuation">,</span> <span class="token punctuation">{</span> excludeExtraneousValues <span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token keyword">as</span> <span class="token constant">E</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">createUserEntity</span> <span class="token punctuation">(</span> user <span class="token operator">:</span> User <span class="token operator">|</span> <span class="token keyword">null</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// user param has User type (same structure with DB table)</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token operator">!</span> user <span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token keyword">null</span> <span class="token punctuation">;</span> <span class="token keyword">const</span> entity <span class="token operator">=</span> <span class="token keyword">this</span> <span class="token punctuation">.</span> <span class="token function">createEntity</span> <span class="token punctuation">(</span> UserEntity <span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token operator">...</span> user <span class="token punctuation">,</span> detail <span class="token operator">:</span> user <span class="token punctuation">.</span> userDetail <span class="token operator">||</span> <span class="token keyword">null</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">return</span> entity <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Here I use the createUserEntity
method to create UserEntities for the domain layer, with the input parameter being user
with data type User
– this data type has the same structure as User Model
or in other words it is data data that I pull directly from the DB.
In the createUserEntity
method, I use the plainToClass
function of the class-transformer
library with the purpose of “forcing” User Model
to become UserEntity
to get the output I want.
End of part two
So in this second part, I have presented to readers the commonly used architectural styles with DDD that are:
- 3-layer architecture (3 layers architecture).
- Layered architecture.
- Onion architecture.
- Hexagonal architecture (Port and Adapter architecture).
- Clean architecture.
as well as other important concepts often mentioned in the domain layer of DDD such as:
- Value-Object
- Domain Services
- Repository
- Factory
Due to the limited knowledge and scope of the blog, I can only present to you the most important and summary content, hopefully they will be more or less useful for you later. Thank you very much for reading and see you in the third part of the series of blogs about DDD.