Domain-Driven Design (DDD) Best Practice with Node.js, MongoDB, and GraphQL

Johnny Yin
Sapia
Published in
5 min readJun 10, 2021

Introduction

Traditional web development normally uses layer-based structures such as controller, service, repository, utils with models. Many popular web frameworks (e.g., nest.js, express.js) create their demo code in this way, which actually misleads many developers especially graduates and junior developers. It seems very straightforward and very quick to start if your app is just simply CRUD from DB. However, as the business becomes complex, you will find your service layer and utils become very large, messy, unmaintainable, and hard to write unit tests. If the code owner leaves the team, new engineers might feel they are touching shit.

Is there any way to solve this issue? Yes, the answer is Domain Driven Design (DDD). You might have been heard of this concept for a long time. Today, I am gonna explain it in detail with some practical code. Actually, the traditional web development looks like an Anemic Domain Model in DDD, which should be avoided (https://stackoverflow.com/questions/23314330/rich-vs-anemic-domain-model ).

Common Concepts

Objects

Before we start, we need to introduce some concepts regarding objects in DDD. In traditional web development, you might only use model (i.e. ORM framework) for data hosting and DB persistency. In DDD, there are many objects: Data Object (DO), Data Access Object (DAO), Entity (Domain Model), Value Object, Aggregate Root, Data Transfer Object (DTO), Domain Event. Below is a brief explanation of the 5 most important objects:

  • DO: a data class we defined for mongoose schema. It reflects what we stored in the DB, such as UserDO
  • DAO: a model class provided by mongoose or other ORM with DO as the generic type, such as Model<UserDO>
  • Entity: a business class object that we define to process business logics, such as User. In GraphQL, we normally directly return entity to the front-end since GraphQL can automatically do batch requests and field selection.
  • DTO: a validation class object that we define to transfer/validate request attributes, such as CreateUserDTO and UpdateUserDTO. This is because updating users and creating users might need different attributes such as ID.
  • Domain Event (optional): an event class object that reflects the side effects of a business logic operation, such as CreateUserEvent. It’s very useful in microservices architecture.

Why we need so many objects? There are many reasons and below are some points that I feel important.

  1. DO vs Entity: Sometimes what we stored in DB is not a business entity and it will need some aggregation or transformation. This is very common in SQL databases such as 1:N or N:N relationships.
  2. DO vs DAO: In DDD we can still utilize DAO to avoid write raw SQL queries even if you might use repository pattern (will be explained later).
  3. Entity vs DTO: In most cases, when creating an entity, we won’t send all attributes in from UI or API such as primary key, IDs, created time, etc. Instead, we probably only allow users to send some non-key information such as name or title. This is way we normally separate Entity and DTO.
  4. Entity vs Value Object: Entity is a primary object that has significance (normally has an ID) such as User while Value Object is more like a complex attribute such as Phone and Location which can have some validation logic inside.
  5. What’s Aggregated Root: In my word, you can assume an aggregate root is a top-level entity within its context. In a blog app, there might be some entities such as Post, Comment ,Like, where Post is an aggregated root becauseComment and Like normally rely on a Post (https://stackoverflow.com/a/1958722/3326887 )

Note that we don’t have to differentiate Entity, Value Object, and Aggregate Root when creating naming conventions even if some engineers might recommend doing it. It’s actually a trade-off, but based on my experience, DO, DAO, Entity, DTO, and Domain Event are good enough to supply a good practice. So there is no need to waste too much time in strictly following DDD which might be dogmatic somewhat.

Layers

Next, we will introduce some concepts regarding code structures. In general, DDD introduces three layers:

  • Infrastructure Layer: handle DB persistence, cache, external service, etc, such as DAO and repository
  • Domain Layer: handle business logics such as domain services, entity (domain model), domain events, etc. Here business logic refers to some complex operations (e.g., money transfer). If your app is just simply CRUD database, there is no business logic.
  • Application Layer: handle the orchestration of domain layers, such as application service. For example, call user.register(userInput); event.dispatch(userRegisterEvent)

Note that there are two different services: domain service and application service. Actually, in normal CRUD apps such as services provided by nest.js, it refers to the application services. In most cases, we don’t really need a domain service if the business logic is very simple. Instead, we only need to put some simple logic in the entity (domain model).

Below is a very general code infrastructure of a user management app.

There are some debates regarding which objects should a repository return? DO, Partial DO, Entity, or Partial Entity? (https://stackoverflow.com/questions/49963065/why-not-use-a-repository-to-return-partial-domain-model-results ). For example, you have a user entity that might have 30 attributes but only need to display three attributes on a user list page with page size 100. In DDD concepts, it requires a repository to return the entire entity and filter attributes when mapping to a DTO object. However, this is not practical considering the database loading and transferring performance. In this case, I’d like to create a separate entity such as UserBasic to represent the most common attributes for the list page. This might be similar to another concept — Command Query Responsibility Segregation (CQRS). Unfortunately, DDD is not a bible and you have to optimize it based on your use cases.

Code Example

<Will be added later>

Reference

--

--