Skip to content
Carrotly Technologies

Get rid of tightly coupled modules and circular dependencies in NestJS

How events help NestJS modules stay within their borders — and get rid of circular dependencies without sprinkling forwardRef() everywhere.

Tomasz Kisiel 5 min read
Get rid of tightly coupled modules and circular dependencies in NestJS

NestJS is a great Node.js framework that brings a lot of fresh air into the ecosystem of Node backends. With its robust module system, it allows planning and building a scalable architecture made of modules that wrap related logic together. While working in a modular environment like this, you may sometimes run into circular dependencies caused by tightly coupled modules. In most cases you will recognise them as the error below.

[Nest] 2788 - 06/10/2023, 12:56:50 PM LOG [InjectorLogger]
> Nest encountered an undefined dependency.
> This may be due to a circular import or a missing dependency declaration.
[Nest] 2788 - 06/10/2023, 12:56:50 PM ERROR [ExceptionHandler]
> Nest can't resolve dependencies of the UserService (?).
> Please make sure that the argument dependency at index [0] is available in the UserModule context.

It is caused by two services that depend on each other to perform their logic. In the example above, PostService is a dependency of UserService, but UserService is also a dependency of PostService. Nest’s dependency injection container is unable to resolve this situation.

As per Nest’s documentation, you may of course use the forwardRef() function. However, this is only a temporary fix. If you don’t truly solve the problem of tight coupling and circular dependencies, adding new modules will become quite painful, and you will end up wrapping most of your dependencies with that function.

This article proposes another solution, which may not apply to every case. But even if it solves only half of your circular dependencies, it is already a good step forward. So, without further ado, let’s look at an example problem and the proposed solution.

The problem

Let’s consider a not-too-simple application for writing blog posts. Apart from just creating posts, we were asked to implement a bunch of additional actions: sending notifications to the author’s followers, increasing the author’s reputation after a post is created, charging for posts behind a paywall and a few other fancy features. In the end, we may end up with code similar to the following.

class PostService {
  constructor(
    private readonly postRepository: PostRepository,
    private readonly userService: UserService,
    private readonly reputationService: ReputationService,
    private readonly notificationService: NotificationService,
    private readonly trackingService: TrackingService,
    private readonly paymentService: PaymentService,
    private readonly moderationService: ModerationService,
  ) {}

  public createPost(args: CreatePostArgs): Post {
    const post = this.postRepository.create(args);

    this.reputationService.increaseReputation(args.userId);
    this.notificationService.notifyFollowersAboutPost(post);
    this.userService.updateUserActivity('post.created', post);
    this.trackingService.registerTrackable('post', post);
    this.moderationService.checkPostContentForViolations(post.content);

    if (args.isPremium) {
      this.paymentService.chargeUserForPremiumPost(args.userId);
    }

    return post;
  }
}

This example is obviously made up, so please don’t pay too much attention to the details. The point is to notice how many services PostService depends on, and to imagine that these services may also depend on PostService. For example, the notification service may need extra data from the post service to dispatch notifications, or ModerationService may need to modify the post again via PostService after moderation. This is where circular dependencies appear.

The solution

The proposed solution is the well-known concept of event-driven architecture. Instead of calling every follow-up action from the createPost method, we simply create the post there and emit an event. Any module interested in reacting to that event can then do so without crossing its logical borders.

NestJS already ships handy tools for working with events. If you don’t have the package installed yet, add @nestjs/event-emitter to your application.

yarn add @nestjs/event-emitter

Once the package is installed, add the EventEmitterModule to the root module of your application.

import { Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    // ...
  ],
})
export class AppModule {}

Now we can get rid of all the PostService dependencies and replace them with just one — EventEmitter2. We can also remove the follow-up method calls from createPost and emit an event with its key and payload instead. The payload may be a separate interface or class if you wish, but for presentation purposes we simply emit the created post.

class PostsService {
  constructor(private eventEmitter: EventEmitter2) {}

  public createPost(args: CreatePostArgs): Post {
    const post = this.db.posts.create({ ... });
    this.eventEmitter.emit('post.created', post);

    return post;
  }
}

The last step is adding a listener to the modules interested in this event. In the notification module, for example, we may have the listener below. In other modules the code will look much the same — only other services will handle the event.

class NotificationsListener {
  constructor(
    private readonly notificationsService: NotificationsService,
  ) {}

  @OnEvent('post.created')
  handlePostCreated(post: Post) {
    this.notificationsService.notifyFollowersAboutPost(post);
  }
}

This way NotificationService and PostService are only loosely coupled. PostService no longer depends on NotificationService. We got rid of the circular dependency, and the functionality still works — yay!

Summary

In this article we explored introducing events into your application to solve circular dependencies. This approach helps modules stay within their borders while still reacting to actions performed in other modules. It may not fit every case, but it definitely fixes the one presented today.

Keep reading

  • Engineering · 16 min read

    Securing CouchDB with Keycloak behind Nginx reverse proxy

    Single Sign-On for CouchDB with Keycloak as the identity provider and Nginx as a reverse proxy — user management moved out of the database and into a proper IAM.

Let's build your product together

Tell us what you're building — raw notes are fine. In a 30-minute call you'll get honest feedback, a rough timeline and a budget range. No pressure, no sales pitch.

  • ✓ Reply within 1 business day
  • ✓ NDA on request
  • ✓ Free estimate
Contact us