#Non domain-based structure

22 messages · Page 1 of 1 (latest)

stray orbit
#

Hey Folks! I want to brainstorm something with y'all regarding module boundaries/project structure.

Let's imagine that I'm building a Modular Monolith that has two different Bounded Contexts: one Users and one Purchase. In a micro(or macro)service world, these could be two different services. In a Modular Monolith approach I'm making them 2 different modules in NestJS. They only export their Use Cases. Each module has a transaction boundary that cannot span beyond that module.

Now, inside User and inside Purchase a similar situation can happen. You can keep dividing by domain (if you want) but let's assume that this is not the case. What would the structure for these modules look like? We want to still to have some kind of structure for modules, but these modules do not form a "bounded context", meaning that a transaction may span sub entities.

What are your thoughts on this?

round vessel
#

Hi Richard. What is a "transaction boundary"?

Structure inside a module is pretty much fair game to whatever you wish it to be, but I believe this is the place to structure to match the components used in Nest modules. So for instance, you'd have controllers, guards, use-cases (otherwise known as services in Nest), interceptors, utils or other providers, tests, etc. If a module is becoming unwieldy in the amount of components it has, then you can "break out" the components into their own module or sub-module. A main concern you can consider while doing so is by asking the question, "can this be re-used in another project by chance?" If the answer is "yes", then the refactoring to break out the code into its own module is doubly important (and useful). 🙂

I wrote an article about this. It doesn't go completely into internal module structure, but it might help you nonetheless. 🙂

https://dev.to/smolinari/nestjs-and-project-structure-what-to-do-1223

DEV Community

So, you're just getting going with Nest, ey? You might not yet completely understand why you want to...

ocean parrot
stray orbit
#

Thanks Scott for your answer! I mean the following: if you're building a Modular Monolith, each module will get its own database connection (most likely). This means that I cannot have a Use Case inside an specific module that spans entities from other Modules because there's no way to atomatically store them. Therefore, the Use Case will call a Use Case on a different module (like you'd call an API endpoint if it were a microservice) and compensate any failure. This means that module has a clear boundary, and what can be reused it's only it's facing API (you wouldn't import any internal service from this module).

In your example, which is more like a Monolith, you have Publishing Module and Drafting Module. These modules are dividied like this because they provide a subset of operations related to Publishing and Drafting. But if both modules operate on a Post entity, then who owns the Post entity and the domain operations that are possible on this entity? Certainly it needs to be defined in a different module (like a single Post module?). Therefore it leads me to believe that there is a domain module somewhere that defines Post and its domain operations (this is an example, but it's something you'd see):

class Post {
state = 'unpublished';

publish() {
    this.state = 'published';
}

draft() {
    this.state = 'draft';
}

}

Unless you tell me that Drafting and Publishing operate on different domain entities. Am I explaining myself? Sorry for the long message.

river wadi
#

Very mature way of finding boundaries. When it comes to dipper separation we come to a subject of architecture styles.
You can keep those modules and that's fine, you can slice them to layers and you have more clear abstraction (controller has a post method endpoint, service has add method and db has insert, not mixed up) but it also can be done using a single module.
However when you want a more powerful setup for experimenting and testing then you should consider a hexagonal architecture for a specific modul (not all of them, case by case)

stray orbit
#

Thanks Maciek for taking the time to answer! Yes, architecture style is also a decision based on needs, everything has its tradeoffs. People will use something like TypeORM and you have an entity (which acts more like an Aggregate) and this entity will have a behavior and business rules (unless you are making a CRUD application). Someone needs to own that. I believe most decisiones are domain-based (e.g.: if you split a big app into smaller services, the domain will be your guiding point. You can still not do it by domain, but then it takes teams to synchronize and discuss changes on the shared domain, trading off freedom).

If we keep Publishing Module and Drafting Module, and these modules are imported in the parent Blog Module but not exported outside then maybe there's not so much use of the module itself as a module. The components could actually be registered directly in the parent Blog module and maybe a flatter structure would be of most advantage.

Thanks for participating in this brainstorming!

ashen dagger
#

In my experience, what people used and called "vertical slice" never work for my cases.
We follow Clean architecture instead, no "modular", but one monothlic layer of pure domain.
NestJS is just the framework layer which supports DI in the service layer.

#

Said other way, "NestJS is not my application".

round vessel
ashen dagger
round vessel
#

I get the point you are trying to make, but Nest does more than just "DI in the service layer". In fact, to answer your question, it helps you organize domain and business logic into pragmatic "modules". That's it's main purpose.

river wadi
#

What do you mean by a pragmatic module?

round vessel
#

It explains what pragmatic modules are. 🙂

#

It doesn't use that term though.

stray orbit
#

When I think about modules I think of it as like a fully qualified Java package. The only difference is that whatever is exported I don't need to instantiate it because it's already done by the framework. Per NestJS docs, a module is a closely related set of capabilities. The selected architecture you want to use in your project is what drives the organization of domain or business logic. A domain-based architecture or a layered architecture will yield different package (or modules) architecture. Each module has a set of own capabilities (controllers and providers) and exposed capabilities/APIs (exports).

If you are using a layered architecture (meaning that your root layer is not the domain but a technical concern) then you'll have modules in each layer that export the related functionality (in the business layer nor persistence layer there won't be any controllers).

If you are using a domain-layered architecture then the root layer is a domain or subdomain. You can still subdivide this domain further and the exposed API should be exclusively the Use Cases of that domain (since the input ports, like a controller or a listener would be registered inside each domain module).

My original question was after a debate: if you are building a Monlith, if you want to use a transaction that spans entities that go beyond one subdomain, then was the best approach to structure the modules? I've always used a domain-based structure so I was trying to understand other alternatives. In this case, it seems that a layered architecture works best to organize the modules since you have use cases that cross domain boundaries (if you want to keep it clean).

round vessel
#

if you want to use a transaction that spans entities that go beyond one subdomain, then was the best approach to structure the modules?
Is that a subject for discussion about modules are about the transaction? I'd want to know what transaction needs to work across or rather span so much of the application and if that is absolutely necessary.

stray orbit
#

Oh no, not necessary at all, it's only a brainstorm discussion. I'd like to be able to lay down some rules on how to guide the architecture of the modules based on the app needs.

#

But it could happen very early on: imagine a Monolithic Bank application. You have the option to create a Contact (so you can transfer money later) and Transfer Money. Imagine you have Contact Management as a subdomain, and then you have Transfer as a different subdomain. Each one has its own Use Cases (like CreateContact and TransferMoney). Now, what happens when someone says that they want a Use Case that creates a contact AND transfers money to that contact. If Transfer fails, the Contact needs to be removed. You could use the Use Cases interfaces to compensate any failing transaction (e.g.: if TransferMoneyUseCase fails, then i'll call RemoveContactUseCase). If you have a single database (like in a Monolith) you could put transfer and contact creation in a single transaction then you wouldn't need compensatory actions. But if that's the case, then a codebase divided by domain may not be the right approach. That was the hypothetical situation that was thinking about. Of course it's not real for a Banking application but I was trying to understand other structures for a possible situation like this.

ashen dagger
# stray orbit Oh no, not necessary at all, it's only a brainstorm discussion. I'd like to be a...

The problems with vertical slicing by using modules to separate domains is many. Some of them as i've seen in many Nestjs codebase, is the coupling between modules will become complicated, sometimes you'll get the self-recursive dependence issue between modules.
It also leads to the problem you raised above, when you want to compose modules functionalities under one transactional boundary.
Next problem, is, requirements flexibility. You can create a perfect business module on day 1, but on day 2, requirements change, which requires big refactorings (and dependent modules too).

#

Until they're solved problems, i don't see treating modules as business boundaries useful in practice though.

#

My assumption here is, NestJS is used to build complex, long term solution. So it needs a first good structure from beginning and require less changes in an Agile project.