The idea of the Domain Driven Design in Angular perfectly presented and fully explored by Manfred Steyer in his DDD series. I won’t rewrite here all the theory and will leave it to your own revision of that great work. In this article, I will show my vision of its implementation with Nx-based monorepo.
Law and order
The main idea is to divide your application by the self-contained parts that we are going to call domains.
As the result, we will have organized structure instead of pile of libraries. Every domain will have the libraries inside it to serve its purpose. From now on, at least two tags will accompany every new generated library: domain
and type
. As you already understood, the domain
tag will hold the domain name this library belongs to, and the type
will label the category of the library. I suggest using these kinds of categories:
Category | Description | Allowed dependencies |
---|---|---|
domain-logic | Main logic of the domain. Contains of services, stores and entities data structures. Must provide façade services for maintaining encapsulation. | util |
feature | Use case implementation. Contains of page and container components. Refers to domain-logic for data and calculations. | ui, domain-logic, util |
ui | Collection of presentational components used in the domain features. | util |
util | Collection of helper functions and classes. Usually it must be pure functions in separate file every, to improve the tree shaking functionality. | n/a |
To provide this strict dependencies allowance we must set these rules in .eslintrc.json
in the root of the repository.
...,
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:domain-logic", "type:util", "type:data-access"]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": ["type:ui", "type:domain-logic", "type:util"]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:util"]
},
{
"sourceTag": "type:domain-logic",
"onlyDependOnLibsWithTags": ["type:util"]
},
]
}
],
...
Domain or not domain
Must we create the domain for every functionality of the project? No. Domain is self-contained reusable part of the application that includes domain logic and at least one lazy loaded feature. There is no sense to create separate domain for every collection of service consumed by applications, it can be the standalone libraries or it can be domain named shared
that will unite all these libraries with category data-access
. In last case, we will need to add this category to linting rules.
The domain
Practically domain itself is a folder inside libs
folder of monorepo. Inside this folder, we will collect all of the libraries belong to this domain.
So, let’s create one. To start new domain we need to create the library named domain
inside directory with our new domain name. Let’s call it feature1:
$ nx g library domain --directory=feature1 --tags="domain:feature1,type:domain-logic"
Congratulations, new domain named feature1
was born.
Now let’s create the library that will hold our features (lazy loaded pages and other container components):
$ nx g library features --directory=feature1 --tags="domain:feature1,type:feature"
Let’s create page called page1
inside features:
$ nx g m page1 --routing --project=feature1-features
$ nx g component page1/page1 --flat --project=feature1-features
This will create folder page1
inside feature1/src/lib
with new module and container component called page1
.
Now, when we have our first container component, it will apparently need some data, maybe API calls. Time to prepare it inside domain logic library.
Domain Logic
Domain logic (DL) library is a heart of our new domain. Unlike domain features, it usually doesn’t make sense to have more than one domain logic. The structure of DL supposed to include at least three folders: application
, entities
and infrastructure
.
Folder name | Description | Is exported? |
---|---|---|
application | Should hold façade services. I recommend creating separate façade service for every feature according to its needs to keep the principle of providing only the data customer demands. Definitely, if different features using similar data there is sense to use the same façade. | Yes |
entities | Should hold interfaces, data classes, models, constants and injection tokens. The decision about exporting this folder depends on the demand of these data structures outside. | Yes/No |
infrastructure | Should hold all calculations, data access services, guards, interceptors, stores and state management. I don’t recommend to export this folder, keep it as a private of the domain and provide access through the façade services. | No |
As an example, we’ll create one infrastructure service and one façade for our page1.
$ nx g service infrastructure/feature1 --project=feature1-domain
$ nx g service application/page1-facade --project=feature1-domain
UI
UI library is the place where we are going to store our presentational components utilized by multiple features of the domain. It can’t be dependent on domain logic or features because neither service can be injected in presentational component. Additionally, this is the good place for Storybook.
I prefer to create every component with it’s own module in separate folder as ng-package
. Let’s create UI library:
$ nx g library ui --directory=feature1 --tags="domain:feature1,type:ui"
To be able to import separate packages unlike the whole ui library, we need to make correction of the tsconfig.base.json
in the root folder of the repository:
paths: {
"@<org-name>/feature1/ui/*": ["libs/feature1/ui/src/lib/*"]
}
Conclusion
The Domain Driven Design gives us perfect tool to bring an order into single page applications becoming every day more and more complex. It allows safely sharing the development process between different divisions and still having consistent application.
Of course, it adds much more work and boilerplates but it will be rewarded in future maintenance.
Top comments (5)
Solid approach with good SoC, thanks for sharing 👍
Though the
domain-logic-
structure looks a bit verbose to me. Why not decouple it tomodels-
,store-
,data-access-
(or alike - for data access literally - API services and stuff like that - or even combining it with thestore-
- which makes total sense IMO)? The business-logic services can be placed inside correspondingfeature-
libs, to avoid traversing the whole folder tree searching for them.nx
is already too nested and verbose so deepening it on is too much for me.It's just an opinion anyway, I saw a lot of
nx
projects and all of them are totally different indeed 😁 Though the domain-driven separation is almost always a good start 👍I think this article could benefit from using concrete domains (e.g., booking) and list the directory structure created. It’s hard to keep a mental model of generic words like feature, feature1, and page1 as they also mix with the type categories.
However, it’s nice to see DDD being applied to an Nx project.
Nice article 🎉
Another good article but a concrete example on github would really add to this article
Keep sharing!!