DEV Community

loading...

Implementing SOLID and the onion architecture in Node.js with TypeScript and InversifyJS

Remo H. Jansen on April 10, 2018

In this article, we are going to describe an architecture known as the onion architecture. The onion architecture is a software application archite...
Collapse
jbogard profile image
Jimmy Bogard

So just an FYI - the project that originated the definition, I was the tech lead on, and we ripped out this style of architecture after about 6 months. For us it didn't scale with complexity. I talked about what we moved to at NDC vimeo.com/131633177

The long and short of it - layers don't encapsulate, they abstract. When we moved to CQRS-style architectures, just the object models that is, we were able to ditch all those service/repository layers in favor of queries and commands. The result was truly SOLID, as opposed to our onion architecture, which I finally saw as a gross misunderstanding of SOLID and OO principles.

Collapse
remojansen profile image
Remo H. Jansen Author • Edited

I just watched your talk and maybe I'm getting it wrong but it seems like you are not happy about the n-tier architecture, not the onion architecture. They are actually quite different (I have also experienced the n-tier pain).

n-tier

onion

More info at blog.ploeh.dk/2013/12/03/layers-on...

One of the things you mention is that you are able to change the used ORM tool. In the onion architecture, this is perfectly possible. In fact, we migrated from sequelize to TypeORM and it was a small job.

When a system becomes more complex, the onion architecture can be combined with the CQRS and the Unit of work patterns and when it becomes even more complex it can be split into "a bag of onions".

Collapse
jbogard profile image
Jimmy Bogard

No, this was an onion architecture, your second picture, not n-tier. Like literally the dude that invented the term, this was the project and we moved away after a few months.

So we start with CQRS and only add any kind of layering like onion as necessary. It's just been 8 years going CQRS-first, and have yet to really need any layers. No abstractions, nothing like a repository or service layer, nothing like that. Just unnecessary.

Collapse
hdennen profile image
Harry Dennen • Edited

"The long and short of it - layers don't encapsulate, they abstract." Effing nail on the head there mate. We've been slowly massaging our front end away from onion because of exactly that over-abstraction to the point of sprint velocity crushing complexity. It also lead to a lot of premature generalization.

Collapse
ajoshi31 profile image
Atul Joshi

I saw your video and seems its like coming back to square one after all the DDD, that that patterns, just simple separation of concern .
In one of my earlier project I was doing dividing the project based on the controller, model and service structure, then in new version I thought the feature separation will help, however eventually I felt that feature separation was making me more concerned as where to keep some files or function.

Like some functions need to be dependent on two or more features, where to create that folder structure, then many a times comes a lot of cross dependability, how those things can be taken care?

In that case I feel that a particular functionality instead of feature works better.

Collapse
alexmreis profile image
Alex Reis

Congratulations, you have now turned JavaScript into a shittier version of Java.

I have had this discussion before with friends that started on dynamic languages and are now enamored with types and the SOLID principles. Dependency injection and nonsense like 300 interfaces are the things that drove half of the Java community to move to Ruby and Python 10 uears ago.

I personally love JavaScript for it's practicality and adaptness. Tieing it up with types is bad enough, and if you add DI and interfaces all around the place, it loses everything that makes JS more interesting as a language than Java or C#.

Coming up next, Fizz Buzz Enterprise Edition in Typescript, because serious node.js code is written by serious businessmen to support serious businesses.

Honestly though, I recommend you read Eloquent Ruby to learn new idioms that make sense on dynamic languages.

Collapse
remojansen profile image
Remo H. Jansen Author • Edited

I respect your point of view but my personal experience is radically different. When I have something like the following:

import { inject } from "inversify";
import { response, controller, httpGet } from "inversify-express-utils";
import * as express from "express";
import { AircraftRepository } from "@domain/interfaces";
import { Aircraft } from "@domain/entitites/aircraft";
import { TYPE } from "@domain/types";

@controller("/api/v1/aircraft")
export class AircraftController {

    @inject(TYPE.AircraftRepository) private readonly _aircraftRepository: AircraftRepository;

    @httpGet("/")
    public async get(@response() res: express.Response) {
        try {
            return await this._aircraftRepository.readAll();
        } catch (e) {
            res.status(500).send({ error: "Internal server error" });
        }

    }

    // ...

}
Enter fullscreen mode Exit fullscreen mode

There are a few things that I wouldn't get without types and without dependency injection:

  • I can do TDD / design by contract with confidence. If I'm implementing a type, the first thing that I can do is click on "Implement interface" on my IDE and it will create a default implementation. If my implementation is a violation of the interface I will get a compilation error.

  • While I implement this controller, another engineer in my team can be creating an implementation of AircraftRepository. We can work in parallel and we know that there will be no integration issues because the interface is a contract between us that cannot be violated by either side.

  • I can use declarative routing @controller("/api/v1/aircraft") without the need to implement a factory for my controller by hand.

  • I have a lot of metadata that can be used for things like generating UML diagrams or Open API definitions.

  • If I need to refactor something I can do it with confidence, a change in the contract will break all the related parts. I know exactly what is left for my refactoring to be completed.

  • I can also use InversifyJS to implement interception (This is not documented in the example but it is supported by inversifyJS). This allows implementing an aspect (e.g., logging) while keeping my entire code base free of logging concerns.

These advantages make a difference for me and I enjoy them. This is why I advocate this approach and I'm sure I'm not alone because there are a lot of emerging Node.js libs and frameworks in this space and some of them are becoming very popular (e.g, nestjs.com/).

Collapse
bmarkovic profile image
Bojan Markovic • Edited

I can agree that if you are used to that way of working and actually enjoy it (personally I'm a J2EE refugee and enjoy the fact that I've left that world of pain), sure, have a blast.

But the thing is that almost every meaningful feature in this approach already has a simpler, more streamlined and more idiomatic alternative in Javascript.

For example, DI is useful for testing. But there are testing-oriented DI implementations for JavaScript that do it much nicer than the square-peg-round-hole approaches like InversifyJS. I'm talking SinonJS, Testdouble.js and Steal.js.

There are numerous AOP solutions for JavaScript like Aspect.js or Meld that are both simpler and more idiomatic than DI inception based ones and things like Express' Debug clearly demonstrate that where AOP is needed it can be added without needless OOP overengineering and it's actually the anyithing-goes, dynamic nature of the language/runtime that massively helps in this regard (higher order functions alone make a world of difference).

In actuality an API is a contract. OO interfaces are really just one way to go about them and not necessarily a better one. But as IDE support is a given using interfaces, I can imagine circumstances where that might be preferable to documenting an API or adhering to agreements, but IRL I've never run across such a situation, yet I've dealt with incredibly slow progress and numerous pain-points in OO-heavy codebases using these patterns (despite working with stellar engineers).

I'm not even interested in going into a debate on typing. The thing is that there are no conclusive proofs that types reduce bugs at all (but abandoning OOP for functional programming apparently does), and one can sprinkle Typescript or native type annotations where one finds them needed without full-on commitment (where lack of strict types has actually helped dynamic languages and JSON to flourish in this world of distributed software and fast-changing APIs).

The "without the need to implement a factory" and "generating UML diagrams" bits in your comment do say a lot too.

Just my 2c.

Thread Thread
godsavethewww profile image
Derek Branizor • Edited

History lesson...Microsoft Windows is built as a bundle of exposed API's -- unlike linux with text config files -- and they caused immense pain in the 90's by breaking their bogus contracts. So I beg to differ, API's are not real contracts. In fact I'm sure the heinous behavior of Microsoft's APIs were one of the reasons Java made interfaces a big part of its paradigm.

Collapse
falagan profile image
Eloy Pérez • Edited

Take it easy. Here, in the article, there´s some work and time invested trying to explain another point of view about node and how to build software in this platform. So, respect for other opinions. I respect what you`re saying about JS and how people work with it taking advance of the flexibility that provides...but this requires a very advanced skills and discipline to maintain the state of the soft clean, stable and readable for others. You must be really good on JS (sure you are), but for others like me who are not that proficient on JS we appreciate solutions like this or Nest that allow us to build (yes, the same house, with the same structure, features etc many times...) projects safely, with rail guidance etc because sometimes you can not wait to be a rock and roll programming star to build and finish an app. With JS you can build custom fancy houses, great... but sometimes you have to build a block hundred identical houses with many workers and in this scenario, Remo solution, fits very well in terms of productivity. I think that it was not Remo´s intention to offend "node's pures engineers" ;)

Collapse
luisnoresv profile image
Luis Nores

So you will probably hate DENO too

Collapse
ydennisy profile image
Dennis

Hi Alex,
I sort of agree with your view. However could you suggest some alternatives patterns to better structure node apps?

Collapse
btomoretti profile image
Mariano Moretti

First of all, great post! But, i can't understand why you shouldn't use java instead of convert javascript into something similar to java. I mean, i can understand why use ts in frontend since you don't have the java possibility.. but, why should i use it in backend? And i'm just asking this because i want to understand and share ideas :)

Collapse
remojansen profile image
Remo H. Jansen Author • Edited

Well, I can enjoy some of the things I like about Node.js / JavaScript / TypeScript:

  • I love JavaScript.
  • I love static types.
  • I enjoy not having to think about threads and deadlocks (non-blocking I/O and single threaded).
  • I like being able to share code between my front end and my backend (e.g., some interface declarations and domain entities).
  • I like the speed of change of the JavaScript community (for some it is fatigue for me it is excitement).
  • I like the fact that Node.js is self-hosted (don't have to deal with tomcat or anything like that).
  • I like that the whole thing is very lightweight (except the node_modules folder xD).
  • I like that being able to execute client-side code on the server side (e.g., React server-side rendering)

But I also get some of the benefits of Java / C#. I had many conversations with many engineers and some of them are completely against things like dependency injection in JavaScript.

They argue that it is not needed because in JavaScript you can monkey patch everything if you need to mock something during a unit test and the same applies to languages like Ruby. For example, you can read this post.

On the other hand, you have people like me or Rob Wormald:

“Typically when I go to JavaScript conferences and I talk about dependency injection people are like ¿What?, ¿What is dependency injection? ¿Why would you do that?... I love coming Build (Microsoft conference for developers) because I can talk about dependency injection and everybody in this room is probably convinced that it is a good idea” - Rob Wormald, Developer Advocate @ Google

This is cool for me, after a long time thinking about this topic (I'm quite passionate about this topic), I have reached the conclusion that it is not right or wrong. If monkey patching works well for you and your team that is awesome. I stand on the other side: monkey patching and lack of static types don't work well for me.

I appreciate questions like yours because as I said I love thinking about this topic :) When I wrote this article I wasn't saying "This is the way you should build Node.js apps", what I was trying to say was "This is the way I've been building very large monolithic apps, with large teams and it has been working very well so far".

Collapse
btomoretti profile image
Mariano Moretti

Yes, I totally understand what you are saying. DI it's pretty cool and it's something that I suffered while I was a working with Ruby. I mean, you can redefine methods or classes at execution time when you need it. But it could be pretty complex at the beginning.

Like you say, there's not good or right for this kinds of topics.
Thanks for your answer!

Collapse
ewnx01 profile image
Drunken Dev

Really nice article and it's about time to introduce methods like this to the JavaScript developers.

Actually I'm not surprised, that some or a lot of JavaScript developers are complaining about it. My experience is that it is very hard for them to understand what design are for and what benefit they have. Especially whe it comes to bigger or more complex applications.
It was, and sometimes still is, the same in other languages. For example I know PHP developers who likes global functions, unclean architecture or everything with static calls, or don't use things like namespaces right.

Collapse
azumelzu profile image
Andres Zumelzu

Hi Remo! Great post! Thanks for sharing!!

Is the source code available somewhere? i.e github, etc?

Thanks!

Collapse
remojansen profile image
Remo H. Jansen Author

Not the one described in this post because is private but I have a small demo is much more simplified and rudimentary but it can help you to get an idea github.com/stelltec/public-tech-de...

Collapse
turcicjosip profile image
Josip Turcic

Wow...Really good work with the blog, finally found an example that totally makes sense. Thank you so much, been searching ages for a decent JS example that ties in with DDD and SOLID. Would love to see the source code for the actual blog. Saw the small demo example and it is very neat. I don't understand why others cant see the benefits of DDD and SOLID. Is there a way to see the private example?

Collapse
azumelzu profile image
Andres Zumelzu

Awesome! Thanks!

Collapse
jonasalbert profile image
jonasalbert

what is the project template did you use in your sample source code? thanks.

Thread Thread
remojansen profile image
Remo H. Jansen Author

The code samples are based on some proprietary code that we created at my current job. I cannot share the original source code as I described already. Sorry.

Collapse
liranbri profile image
Liran • Edited

thank you for a great article!

few points:

  1. don't you think it smells that you need to have your IoC framework decorators inside the lowest level layers such as the domain? Uncle Bob in Clean Architecture specifically suggests to avoid that.. the IoC is also just an implementation detail.

  2. I couldn't find where you use the "Application Layer". isn't it missing?
    for example "create new Aircraft" should not be considered a use-case belongs to the Application Layer?

As far as I understand, domain services should be introduced when you have a domain business rules. but in your case, you put there operations which don't hold any business rules ("get all aircrafts" for example). I think it indicates that it does not belong there.

I even picked at the demo you shared (github.com/stelltec/public-tech-de...) and there is not even a folder for this layer.

on this article:
blog.codeminer42.com/nodejs-and-go...
(you can skip to this code at: github.com/talyssonoc/node-api-boi...)
The guy put those operations in the application layer for example. do you think he's wrong?

Collapse
mpachin profile image
Misha Pachin

Thank you so much for your comment and for your questions and links! It's really helpful, especially this one (github.com/talyssonoc/node-api-boi...)

Collapse
donkeykong91 profile image
Daryl Occ

I think you forgot to add the Serializable interface to the Rectangle class in the Interface Segregation Principle section so that it can use the serialize() method.

Collapse
remojansen profile image
Remo H. Jansen Author

Thanks a lot for the heads up!

Collapse
donkeykong91 profile image
Daryl Occ

Yup yup!

Great article by the way! 👍

Collapse
blouzada profile image
Bruno Louzada

Extremely nice post! Well explained SOLID and onion architecture with good examples, thanks!

Collapse
remojansen profile image
Collapse
brpaz profile image
Bruno Paz • Edited

As someone who loves this kind of architecture, It was a great read!
I need to look more at Typescript, InversifyJS, and TypeORM. It seems a viable alternative to PHP for OOP projects. (Not that I want to replace PHP in my stack, but its always great to have another option, mostly for some projects that could benefit from the NodeJS ecosystem).

Collapse
jackmellis profile image
Jack

Late to the party but really excellent article!

I've worked hard in recent times on implementing onion architecture (and later hexagonal architecture) in a strictly FP front end and I can't imagine going back to a code base organised exclusively by domain.

For reference and self-plugging I created jpex to achieve dependency injection in a similar vein to inversify but with FP at the forefront.

Collapse
lephuocmy668 profile image
My Le Phuoc

Any one know what is @dal?

Collapse
remojansen profile image
Remo H. Jansen Author

DAL means "data access layer" and is an alias for a path. Check this out to learn how to create aliases for paths in TS stackoverflow.com/questions/432817...

Collapse
lephuocmy668 profile image
My Le Phuoc

Hi @RemoHJansen, I'm switching from Golang to Typescript, i dont know to discriminate standard library in Typescript. Please let me know how to discriminate standard library. Thanks you!

Collapse
remojansen profile image
Remo H. Jansen Author

What do you mean by "discriminate standard library"?

Collapse
lephuocmy668 profile image
My Le Phuoc

How do you know what is standard library in Nodejs and Typescript?
In Golang i prefer to use standard library(golang.org/pkg/). If in this list there isn't library which i need, i must pick thirt party library.

Thread Thread
remojansen profile image
Remo H. Jansen Author

In Node.js and TypeScript everything that you install via npm is not part of the standard library. The standard library is knowns as "Node core" and you can check the available modules (they are listed on the left menu) at nodejs.org/dist/latest-v10.x/docs/...

Collapse
lephuocmy668 profile image
My Le Phuoc

@remo H. Jansen

import { AircraftEntity } from "@dal/entities/aircraft";
Enter fullscreen mode Exit fullscreen mode

I can't find out when it was implemented and i can't understand where '@dal' point to. Please help me

Collapse
smile_by21 profile image
zl

Why do you need the EntityDataMapper? why not use the domain entity directly? I am curious about how did this work with typeorm and how did your DalEntities synced with the database ?

Collapse
remojansen profile image
Remo H. Jansen Author

We use the data mapper because we don't want to "pollute" our entire application with data-access concerns. The TypeORM entities are data-access concerns because they are a 1-to-1 map to the database tables and contain database-related metadata added via decorators. We use the domain entities instead in the entire system but the data access layer (DAL) cannot use domain entities to persist them (via TypeORM). So the data mapper job is to translate in and out of the DAL from domain-entity-to-orm-entity (in) and from orm-entity-to-domain-entity (out).

Collapse
jamiecorkhill profile image
Jamie Corkhill • Edited

How do you work with transactions using TypeORM with this architecture? TypeORM seems to want to break Separation of Concerns by using Transaction Decorators.

Thanks.

Collapse
tiennampham23 profile image
Tien Nam Pham

Hi Remo H.Jansen
I don't know what TYPE.TypeOrmRepositoryOfAircraftEntity is, Can you let me know about it?
Thank you!

Collapse
nirav_sanghvi profile image
Nirav Sanghvi

Amazing stuff. Really liked the suggested flow and how it helps in separating concerns.

Collapse
marchiartur profile image
aRTUR

Is there some book to learn SOLID principles?

Forem Open with the Forem app