DEV Community

Harry
Harry

Posted on • Updated on

Applying Domain-Driven Design principles to a Nest.js project

This post originally appeared on my personal blog.

Hey, today I'm going to be writing about how you can apply domain-driven design (DDD) principles to a Nest.js project.

I've created a quick repository showing the architecture explained in this blog. Find it here.

Disclaimer

  • I am by no means an expert in DDD, we merely decided to adopt it for Repetitio when we re-wrote the server.
  • I will not cover what DDD is in this blog, please refer to this dev.to blog post or the DDD bible.
  • Nest.js - A progressive Node.js framework for building efficient, reliable and scalable server-side applications. It mirrors Angular's architecture style and is built with TypeScript from the ground up.

Why Nest?

Nest is opinionated on how to structure your code, this works well in a DDD case as you need to be able to put strict boundaries around your code as to keep your code maintainable and readable. If you are looking for a powerful and scalable framework for your Node.js application, I highly recommend Nest.js.

So why would we do this?

Having used Nest.js in production for about 4 months we ended up, due to the project being unrestricted without clear boundaries, with a mess of spaghetti code for our server and API. We're talking about services that are 600+ LOC long.

This is by no means a reflection of Nest.js, but rather a reflection of what happens to codebases without strict rules and boundaries implemented.

We decided to jump on the bandwagon and start to look into DDD as a way forward after we had decided to rewrite the server.

So ultimately what we wanted to get out of this exercise is a codebase that by nature of design stays tameable.

How do you do this?

We found that DDD outlines a clear structure to your code that not only makes sense but can minimise the amount of code/methods/classes that resides in aspects of your domain.

To use our old server as an example; we had a service called user.service.ts that contained ALL logic for the user. Now, as with most applications, the user tends to take a center stage and encapsulates a lot of domain logic. Therefore, our service became huge, covering the entire user logic. It became hard to read and understand what each method was trying to achieve and what actions it was performing.

So let us use this example and apply some DDD logic to it, firstly we decided to be very strict with our domain layer, therefore in the user domain we only care about our user domain model, all other relationships are abstracted out into a new domain layer or a subdomain.

We kept the usage simple and made a class per action, rather than lumping them all into the same omnipotent, omniscient service. By the fact of simplifying, our domain layer becomes very functional with each class performing typically one action, with the methods within further reflecting this.

Our architecture

This is our architecture: (using a very generic user as an example for the different files and services)

src/
    /API
        /User
            UserController.ts
            CreateUserDTO.ts
        APIModule.ts
    /Auth
        AuthModule.ts
    /Database
        DatabaseModule.ts
    /Domain
        /User
            CreateUser.ts
            IUserRepository.ts
            User.ts
            UserModule.ts
    /Persistence
        /User
            UserRepository.ts
            UserRepositoryModule.ts
            UserEntity.ts
    /Utils
        /Mappers
            /User
                CreateUserDTOToUser.ts
        /Services
            /Email
                EmailSenderService.ts
    AppModule.ts
    Environment.ts
    main.ts
Enter fullscreen mode Exit fullscreen mode

The Module in this case is a Nest Module. We decided that each Domain would have its module, as would each persistence entity. However, we decided to lump all API endpoints into one module, as the only thing importing the APIModule was the AppModule on application bootstrap.

API

This layer contains all our endpoints and controllers. We tried to make a Controller class mirror a domain entity. So, if we had a user domain, we'd have a user controller. This pattern extends to the persistence layer too.

This is also an obvious space to declare any DTOs - sending or receiving.

Auth

This is where all our Guards, Strategies and general Auth configuration lies.

Database

You guessed it, this layer is responsible for connecting to any kind of data store, or multiple data stores for that matter.

Domain

The most important part of our codebase. The domain is a reflection of the problem we are trying to solve. This is broken up into our domain models (each warranting their folder). Our domain model, in our case, is a TypeScript types file.

We want to keep our domain layer pure from third party artefacts, so within our domain we should only reference our code. We don't want to see MongoDB schemas, third-party packages, reference to any database-specific logic or anything related to our API layer.

We define an interface that mocks our repository layer. This is defined in our domain layer as it is directly related to our domain. We are outlining how we want to be able to mutate our domain models, we are not bothered with the implementation (that is the persistence layer's job).

A few rules we adhere to with our domain layer:

  • The domain actions should only accept the domain model as a parameter or an Id in string form. DTOs should be mapped before calling our domain layer.
  • Leave all third-party libraries, packages etc outside the domain layer. It should be third-party dependency-free.
  • It should only reference code that exists in the domain layer
  • In theory, you should be able to cut and paste your domain layer it into any project (language-dependent) and it should work.

Dependency-free domain

One of the biggest mental challenges is organising your code in such a way that your domain layer is only dependent on other classes and files within your domain layer.

For example, to communicate with the persistence layer we can introduce the UserRepositoryModule as a dependency to our UserModule but would go against a key component of DDD - a dependency free domain. It's also why we have a User.ts (domain) and UserEntity.ts (persistence). One is our domain model, in its purest form. The other, our domain model but with the added attributes/functionality for whatever data store.

One way (and thanks to SeWaS for showing me how) we can use interface injection, rather than typical module injection to communicate with the persistence layer.

DomainAction.ts is just a generic name which represents the many actions our domain will perform.

// Domain/User/DomainAction.ts
import { Injectable } from '@nestjs/common';
import { Injectable, Inject } from '@nestjs/common';
import { UserRepository } from '../../Persistence/User/Repository';
import { IUserRepository } from './IUserRepository';

const UserRepo = () => Inject('UserRepo');

@Injectable()
export class DomainAction {
    constructor(
        @UserRepo() private readonly userRepository: IUserRepository,
    ) {}
}
Enter fullscreen mode Exit fullscreen mode
// Persistence/User/UserPersistenceProvider.ts
import { Provider } from "@nestjs/common";
import { UserRepository } from "./Repository";

export const UserRepoProvider: Provider = {
    provide: 'UserRepo',
    useClass: UserRepository
}

Enter fullscreen mode Exit fullscreen mode
// Persistence/User/UserRepositoryModule.ts 
import { UserRepoProvider } from './UserPersistenceProvider';

@Module({
    providers: [UserRepoProvider],
    exports: [UserRepoProvider],
})
export class UserRepositoryModule {}
Enter fullscreen mode Exit fullscreen mode

Domain structure

Each domain action that gets performed on our domain model should constitute its own file and class. The name of this class should be explicit and leave no-one guessing as to its purpose.

For example:

/Domain
    /User
        Create.ts
        Update.ts
        Delete.ts
        GetEmail.ts
        RemoveToken.ts
        IUserRepository.ts
        UserEntity.ts
        UserModule.ts
Enter fullscreen mode Exit fullscreen mode

We can even drop the User from the name of the class as it is implied due to it residing within the user domain.

Persistence

Our persistence layer is where all our database queries are performed. This will contain the entity's Module and its Repository. The repository in this sense is a class that contains all database operations. Again, this should mirror our domain entities 1:1 and should typically only contain around 4/5 methods - predominately CRUD operations. Unlike our domain layer, we couple multiple actions within the same class.

Utils

These typically share functionality required across our domain.

This is where we store all our mappers that map data-transfer-objects to domain models and vice versa.

It is a good place to store singleton services, like the example, an email sender service.

Typical flow through our server

As I feel the need to show some code, I'll show some snippets of how this all looks if we were to create a very basic user in a purely fictional server. Obviously, I'm excluding import statements here.

// the user DTO we receive from the client
export class CreateUserDTO {
    @IsString()
    @IsNotEmpty()
    public name: string;

    @IsString()
    @IsNotEmpty()
    public password: string;
}
Enter fullscreen mode Exit fullscreen mode

Class validator is great for validation, couple that with an AuthGuard (in the Auth layer) and you can handle all exceptions for your code in one place and handle the response object.

// UserController.ts
@Controller('user')
export class UserController {
    constructor(
        private readonly user: Create,
    ) {}

    @Post()
    public async Register(@Body() createUser: CreateUserDTO): Promise<HttpStatus> {
        // all our mappings get done in static classes
        const domainModel: User = UserMap.mapCreateDTOToUserModel(createUser);
        await this.user.Create(domainModel);
        return HttpStatus.OK;
    }
Enter fullscreen mode Exit fullscreen mode

As our domain is only concerned with our domain we need to make sure that all API layer related objects get mapped to the domain appropriate model, hence our mapping happening in the API layer.

// /Domain/User/Create.ts
const UserRepo = () => Inject('UserRepo');
const EmailService = () => Inject('EmailService');

@Injectable()
export class Create {
    constructor(
        @UserRepo() private readonly userRepository: IUserRepository,
        @EmailService() private readonly email: IEmailSenderService,
    ) {}

    public async Register(user: User): Promise<void> {
        const registeredUser: User = await this.repository.Create(user);
        await this.email.SendEmail(registeredUser.email, EmailOptions.AccountCreationEmailOptions, {
            userId: registeredUser._id,
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

The EmailSenderService is an example of shared logic across our domain that would exist in the Utils layer mentioned above.

// /Persistence/User/UserRepository.ts

@Injectable()
export class UserRepository implements IUserRepository {
    constructor(@InjectModel('User') private readonly user: Model<UserEntity>) {}

    public async Create(newUser: User): Promise<UserEntity> {
        return new Promise<UserEntity>((resolve, reject) => {
            const createdUser: UserEntity = new this.user(newUser);
            this.user.create(createdUser, (err: GenericError, addedEntity: UserEntity) => {
                if (err) {
                    reject(err);
                }

                resolve(addedEntity);
            });
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

We can also see the need to have two types of User, one for our domain which is the purest form of our domain model and one that exists in our persistence layer that would be very similar to our domain model but would contain third-party (in this case, database-related) attributes or logic specific to our persistence layer.

This further re-enforces the need for our domain layer to be pure of all third-party artefacts.

Summary

This is a brief overview of the architecture we've employed at Repetitio. By holding ourselves to such rigid rules we've found our codebase has been pleasantly manageable, nothing like it was before! With clear logical layers to our server, it is easy to navigate and understand, allowing us to easily dive in and iterate with an ever-evolving set of requirements.

I've created a quick repository showing the architecture explained in this blog. Find it here.

Top comments (20)

Collapse
 
smolinari profile image
Scott Molinari

I can't disagree with this direction completely, but I'd like to put in a word as food for thought.

Let's say, I build out an "Auth" module (as in authentication module, but what I'm about to write could be taken in the context with any kind of module) and I realize it is fairly standard for all of my apps and I'd like to make it its own library. This suggested split up of API logic from the modules would mean I couldn't easily pull out my Auth Module to make it a library. It is coupled strongly to the overall app via file structure.

So, I see DDD in a different light with Nest. The parts that make up the API (controllers or resolvers with GraphQL) should be part of each module. This is what Nest prescribes too. Why? Because it also simplifies the next step of scaling up to microservices. Aha! :D

I see each module like their own little DDD onions, because Nest basically takes the little onions (the modules) and aggregates them into a big DDD onion via it's module system. With the suggested methodology here, you are negating that big DDD onion abstraction Nest provides.

Just sayin. :)

Scott

Collapse
 
jbool24 profile image
Justin Bellero

Love the mini DDD onion analogy! I have a project I'm working on that we do exactly this. Each feature "onion layers" maintains its own persistence model. The one thing I find frustrating is finding a way to cleanly exchange services between the mini onion layers to share data that each other references. Orders to Products for example.

Many features need to reference and use each other's data to build DTOs specifically for GraphQL. This gets scary because it could introduce circular dependencies between modules. I know the Nestjs docs talk about how to resolve this with the DI but I'm not satisfied. Having to inject a module to use a service means one module has to "know" something about the other module. In microservices this could be solved by passing Events on a bus or Message Queue but within a monolith that seems unnecessary since it's all running in one process. Wondering if anyone has ideas about using service between modules without creating a hard dependency by injecting the module. That way it can be split into a microservices later with minimal effort if one feature needs to scale more than another. Maybe a pattern doesn't exist unless it's split into microservices, events, and RPCs? 🤔 The only thing I could think (which I hate) is to duplicate domain objects to share access to the data. Yuk

Collapse
 
smolinari profile image
Scott Molinari • Edited

@jbool24 - If you think about it, should the modules really be sharing DTO definitions? I personally don't think so. The DTOs should be defined locally/ independently to the module itself, IMHO. So, with that said, problem one is resolved, I believe(?).

I'm not totally sure what you mean with using DI to resolve the DTO sharing issue. It doesn't solve that if you ask me or rather it shouldn't. DI is for sharing implementations of other services (providers) between other services. And yes, you are right, it is a form of coupling and differs from microservices (which has coupling too, but in a different form).

So, you do get on to a problem I'm also looking to solve. How to build a monolithic system, which can be easily upgraded or "scaled" to microservices and I agree, using DI isn't one of them.

As I see it, this is where CQRS, or rather the "buses" it offers, could come into play. You could use that as the basic interface for both the monolithic modules and microservice architectures, hiding or rather abstracting away the actual communication method (i.e. message broker, pub-sub, internal bus ala rxjs, etc.). Thing is though, for a very small team or a single dev, creating this event driven system will seem like a ton of verbosity. Nest's own modules system can seem like a lot of verbosity at first sight.

I'm still in the design phase on what to do here. If you have any other ideas along on how to abstract the communication between services (which is what Nest's DI system is also doing, as you mention), please let me know. DI is great for a monolith, but doesn't afford the right abstraction to move modules to their own microservices easily. Modules sort of help, because they make you think in a services kind of way, but they only get us half the way there, so to speak. I wish Nest would have been a bit more opinionated in this respect. ¯_(ツ)_/¯

Scott

Thread Thread
 
jbool24 profile image
Justin Bellero

I actually think I solved the design today. At least for the short term that’s going to work for me. Even though the docs warn against global modules I decided that only my feature modules would be decorated with @Global and only “exports” a public service from the module that contains all the public use-case handlers. This way the only reference between bounded contexts of feature modules are in the application layer as globally resolvable injected services to use-cases. Make sense? This way in the future, as I pick apart the features into separate microservices (if ever), all I need to change per module is a remote service definition to satisfy that injection (whether its RPC or http or whatever the now remote service is). Not sure if I articulated that well but the approach frees up importing features to other features while its a monolith. That was the biggest issue for me because it caused circular dependencies that I had to resolve with ModuleRef and forward referencing and that just felt wrong.

Also, I agree about the verbosity (especially as a solo dev) but I do truly believe that your future self will appreciate the the upfront effort keeping things tidy. There is a tendency with JS project for devs to fall back on implementing direct to libraries because they do so much magic for us (looking at you mongoose 😘). And while that’s great for getting a prototype running quickly, it becomes horrible really fast as tech debt adds up and sometimes libraries just stop being supported 😱 This one project I was worked on had extremely tight deadlines so we decided to implement all our domain logic on mongoose models cause, hey it’s all there (validation, virtuals, methods, statics!!, hooks) So we got it running in in time, but later it almost brought down the entire startup when we were told we HAD to switch to a relational DB for various reasons.

That’s where I struggle now when the frameworks try to be too clever for us. I don’t want to always implement from scratch so frameworks help BUT I remember the pain from that experience and I force myself to do the extra work (or at least have a plan) where time constraints allow.

Collapse
 
bendix profile image
Harry

Hey Scott, interesting take and definitley one we made the conscious decision to move away from.

By having your project split into their own modules definitley makes moving parts into packages and eventually into a micro-service architecture easier. However, your domain (the problem you are trying to solve) becomes coupled to the wider module. The domain shouldn't care for anything other than the logic for solving the problem and suddenly our domain gets muddied with controllers, dtos and repositories, when in fact our domain module should be concerned with solving problems.

As the domain is the problem solving element of the code, it is the most important part of the code-base and it is vital to keep that as "pure" as possible. As mentioned in the post, ideally, you should be able to lift the domain from the code and nothing extra should come with it.

I also have no plans to move any of my code into a micro service architecture :) But that is personal preference.

Collapse
 
smolinari profile image
Scott Molinari • Edited

Hi Harry,

I guess we'll have to agree to sort of disagree. But, I'll leave you with a couple more thoughts.

You are right about the business logic being specific to solve the business problem and it needing its specific place in code. And modules allow for that. Yet, I feel it can't also mean the "plumbing" of the "how the code works" should also be forgotten or changed in a way just to make this "put the business logic in it's own special place" concept possible. If the dev team realize that there is this plumbing around the module, where is that "muddying up the domain"? In fact, if the plumbing also needs work, because the domain problem changes, which I think you can agree does happen, all the code is right there in the module together with the business logic and is thus, easier to reason about and change, no searching for matching plumbing code at all.

If you get new engineers who have worked with Nest before, they will understand the "Nest way" at first sight. If you change the code structure away from Nest's default structure, you add cognitive load thus, slowing down their ability to make changes/ maintain the code.

Lastly, if you decide you want to have different programs (not just microservices) and realize some of the code (the modules) are interchangeable (i.e. plugins), the modules system affords this as designed. If you pull the little onions apart to create your own big onion, it is truly the ugly monolith (with all disadvantages) Nest tries its best to avoid.

Scott

Collapse
 
brngranado profile image
Bryan Granado

I'm agree. I believe that can abstract some process, specifically's services using patterns design, for to give each function a unique responsability.

Collapse
 
bendix profile image
Harry • Edited

Afternoon chaps, @guledali , @maxhampton . I've just uploaded an example repository here. Hope it covers enough of your interest, please hit me with more questions if you've got them.

FYI that repo isn't prod ready, you will need to spend some time tweaking passport and mongoose to get the DB working. Figured it would suffice but happy to expand it further if you want :)

Collapse
 
maxhampton profile image
Max

Thanks Harry, the repository is definitely interesting to browse through, and clears up some of my confusion. It's leading me down the road of looking into options for interface injection in Nest.

Kind of a follow up question, I still see the domain layer taking on dependencies from higher levels, namely the UserRepositoryModule. Is this mainly to get around dependency injection limitations?

I'm not a stickler for 100% pure DDD, the code is still very well segmented. I am curious, though, if this is a concession your team made and the rationale behind bringing a persistence layer dependency into the domain layer. Did you consider alternate approaches to keep the Domain layer dependency free?

Collapse
 
bendix profile image
Harry

Hey Max. When we started this process we looked into interface injection but we came up short. For us to have any communication with our persistence layer we had to introduce the user repository dependency into the UserModule.

We decided that our domain layer would:

  1. Be free of all third-party code
  2. Would only accept the relevant domain model as a param or a return type from the persistence layer

We figured this was adequate for our needs.

Do let me know how you get on and if you are able to overcome this.

Collapse
 
bendix profile image
Harry

Hey Max, so it turns you actually can do interface injection (somewhat) with Nest.js.

A lovely fellow on the GitHub repo submitted a PR detailing just that:

github.com/hbendix/domain-driven-n...

I will update the blog post when I get a chance.

Collapse
 
namlt3820 profile image
namlt3820

Thank you, I'm also researching about this topic in particular. I have a question regarding your statement:

The domain actions should only accept the domain model as a parameter or an Id in string form

I can follow this statement easily with create, findOne, update, delete API. But I cant do it with findAll API since I always have to use some sort of object query from persistence layer/library:

import { FilterQuery, ProjectionType, QueryOptions } from 'mongoose';

async getAll({
        filter,
        projection,
        options,
    }: {
        filter?: FilterQuery<T>;
        projection?: ProjectionType<T>;
        options?: QueryOptions<T>;
    }) {
        return this._model.find(filter, projection, options);
    }
Enter fullscreen mode Exit fullscreen mode

You can see that if the domain layer want to use this method, it has to import some interfaces from mongoose, making it library dependent. Is there a way to avoid this? Thank you.

Collapse
 
guledali profile image
guledali • Edited

@bendix I discussed the article with some co-workers yesterday, very interesting do see someone applying DDD to nestjs.

One thing while reading this, here is a quote that kept me curious

due to the project being unrestricted without clear boundaries, with a mess of spaghetti code for our server and API. We're talking about services that are 600+ LOC long.

In nestjs, they recommend organising your application components through modules. Ideally you would have multiple feature modules and each encapsulating it's own business-domain(shoppingcart -> entity, service, controller, spec and dto).

This something that they even state in nestjs docs

A feature module simply organizes code relevant for a specific feature, keeping code organized and establishing clear boundaries. This helps us manage complexity and develop with SOLID principles, especially as the size of the application and/or team grow.

You said the problem you and your team ran into was having too much code in the services file, was this the only drawback?

Collapse
 
bendix profile image
Harry

Hey @guledali , ultimately it came down to poor handling in the early stages of the development. We had neglected to place strict boundaries around our domain logic so something like a user.service encapsulated a far too large chunk of user logic. We should've spilt the code out into further sub-modules to avoid this.

I think the suggested route recommended by Nest.js definitely works. We find following this DDD approach it keeps clear distinctions between all the different processes our server goes through. (endpoints, business/domain logic, data access etc) Whereas following the typical Angular structure it puts more emphasis on each microservice of your server.

I think there are positives and negatives to both.

Collapse
 
maxhampton profile image
Max • Edited

I'm having trouble understanding from this post how you've kept database dependencies out of the Domain layer. If UserEntity is in the Domain layer, and extends the Mongo Document type, doesn't that bring an external dependency on Mongo into the Domain?

Would love to browse a repository to take a look at how this is set up in full, I'm working on a project now where we're trying to do something very similar following DDD in NestJS.

Collapse
 
bendix profile image
Harry

Apologies, still getting used to this blogging malarkey! I have some spare time tomorrow will get something up for you to look at, Max.

Collapse
 
mokel profile image
Mokel

Cool~,Harry. Inspired by your sharing, I created an equally good code structure

Collapse
 
guledali profile image
guledali

@bendix Do you have any github repo containing samples on this?

Collapse
 
bendix profile image
Harry

hey guledali, I can upload a repository this weekend for you :-)

Collapse
 
awojnar profile image
Artur Wojnar

Having User entity in the context of the DDD stands in contrary to the DDD itself