DEV Community

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

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.