Tutorial

Understanding Circular Dependency in NestJS

Understanding Circular Dependency in NestJS

The author selected Free and Open Source Fund to receive a donation as part of the Write for DOnations program.

Introduction

An Image showing circular dependency error at Module Level

NestJS throws this error whenever it detects circular dependency between modules. In this tutorial, you will learn what circular dependency in NestJS is and how you can resolve it.

Circular Dependency occurs when two classes depend on each other. In NestJS, circular dependency can occur between modules and providers. There are different types of circular dependencies and the most common ones that you can run into during development are:

  1. Circular module imports - where module A imports module B, and module B imports module A.
  2. Circular constructors - where service A injects service B and vice versa.
  3. Circular file imports - where file A imports file B and vice versa.

In this article, you will work with a demo e-commerce application comprising two modules: OrdersModule and PaymentModule. Within these two modules are their providers OrdersService and PaymentService. The OrderService initiates the payment process for an order, while the PaymentService needs to update the order status once payment has been processed. This will create circular dependency as module and service level as both resources depend on each other.

Note: To bootstrap your Nest project, run the following command:

nest new circular-dependency

Then run nest g resource orders and nest g resource payment to generate a new resource for orders and payment that will create a module, controller, and service files.

Prerequisites

To follow this tutorial, you will need:

Circular Dependency Between Modules

First, here’s a closer look at the error that is thrown in a situation where OrdersModule and PaymentModule depend on each other.

Nest cannot create the PaymentModule instance.
The module at index [0] of the PaymentModule "imports" array is undefined.

In NestJS, modules can depend on each other, but when you have two modules directly importing each other, it creates a circular dependency, which NestJS cannot resolve. This means that each module is waiting for the other to be fully initialized before it can itself be initialized, leading to a deadlock.

When NestJS starts the application, it first resolves all the modules and their dependencies before creating instances of those modules. Now, NestJs looks through the imports array in the root module app.module.ts and starts to resolve OrderModule. It then sees that it depends on PaymentModule. NestJS pauses the resolution of the OrdersModule to resolve this dependency first because PaymentModule is in the imports array of OrdersModule. When NestJS switches to the PaymentModule, it looks at its import and sees that OrdersModule is listed. Notice that OrdersModule has not been fully resolved yet, NestJS then attempts to initialize OrdersModule as part of resolving PaymentModule. It goes back to initializing and resolving the OrdersModule and it ends up in a loop because OrdersModule is waiting for PaymentsModule to be initialized and PaymentModule is waiting for OrdersModule to be initialized.

Assuming PaymentModule is the only dependency of OrdersModule and not vice versa, during PaymentModule resolve as explained above, NesJS will look at its imports array for other modules to be resolved and instantiate the PaymentModule since OrderModule is not part of its imports array.

Resolving Circular Dependency Error Between Modules

Module Forward Reference

To resolve circular dependencies between modules, we will use the forwardRef utility function on both OrderModule and PaymentModule.

payment.module.ts
import { Module, forwardRef } from "@nestjs/common";
import { PaymentService } from "./payment.service";
import { PaymentController } from "./payment.controller";
import { OrdersModule } from "../orders/orders.module";

@Module({
  imports: [forwardRef(() => OrdersModule)],
  controllers: [PaymentController],
  providers: [PaymentService],
  exports: [PaymentService],
})
export class PaymentModule {}
orders.module.ts
import { Module, forwardRef } from "@nestjs/common";
import { OrdersService } from "./orders.service";
import { OrdersController } from "./orders.controller";
import { PaymentModule } from "../payment/payment.module";

@Module({
  imports: [forwardRef(() => PaymentModule)],
  controllers: [OrdersController],
  providers: [OrdersService],
  exports: [OrdersService],
})
export class OrdersModule {}

Save your code and start up the terminal again. The error should be cleared now:

An Image showing server terminal after circular depeendency has been resolved

In the code above, the forwardRef is used to wrap the imports on both sides to ensure that both modules can be loaded without immediately resolving their dependencies on each other.

At the backend, NestJS starts the module initialization process and when it sees the forwardRef in either module’s imports array, it will not immediately attempt to resolve both modules, rather it acknowledges the dependency but delays its resolution. Both OrdersModule and PaymentModule are loaded into the application’s context without their dependencies being fully resolved to prevent the deadlock. NestJS will go back to the deferred dependencies once all modules are loaded. Now both OrdersModule & PaymentModule have been loaded (not fully initialized), NestJS calls the forwardRef function to resolve all dependencies.

Resolving Circular Dependency Error Between Services

In the previous section, you learned how to resolve circular dependency that occurs at the module level. The second scenario is: in OrdersService you create an order, save it to the database, and then immediately call the processPayment method to process a user’s order. And, in PaymentService, once the processPayment method is called, it immediately calls the updateOrderStatus method to update the order status to successful or any status you might want to denote it with.

Inject these dependencies in both services:

order.service.ts
interface IOrder {
  id: number;
  customerName: string;
  item: string;
  orderDate: Date;
  totalAmount: number;
}
@Injectable()
export class OrdersService {
  constructor(private readonly paymentService: PaymentService) {}

      async getAllOrders(): Promise<IOrder[]> {
    const mockOrders: IOrder[] = [
      {
        id: 1,
        customerName: 'Taofiq',
        item: 'Airpod',
        orderDate: new Date(),
        totalAmount: 900,
      },
    ];
    return mockOrders;
  }

  async createOrder(createOrderDTO: CreateOrderDTO): Promise<any> {
    // mock data to simulate order creation
    const newOrder: any = {
      id: Math.floor(Math.random() * 100) + 1,
      ...createOrderDTO,
      orderDate: new Date(createOrderDTO.orderDate),
    };

    await this.paymentService.processPayment(newOrder.id);
    return newOrder;
  }
  async updateOrderStatus(orderId: string, status: string) {
    // update the order status here in the database
    console.log(`Order ${orderId} status updated to ${status}`);
  }
}
payment.service.ts
@Injectable()
export class PaymentService {
  constructor(private readonly ordersService: OrdersService) {}
  async processPayment(orderId: string) {
    // In a real scenario, you would interact with a payment gateway.
    console.log(`Processing payment for order ${orderId}`);
    const paymentSuccessful = true;

    if (paymentSuccessful) {
      // Once payment is successful, update the order status to "Paid"
      await this.ordersService.updateOrderStatus(orderId, 'Paid');
    }
  }
}

When you save both these files and restart the server, you get this error:

An Image showing circular depeendency between services

This error occurs because the NestJS Dependency Injection system container can not resolve the direct circular dependency between OrdersService and PaymentService.

When the app starts, both modules are instantiated and registered. Then NestJS tries to instantiate PaymentService and sees that it needs an instance of OrdersService to be injected as a dependency. NestJS checks for an instance of OrdersService within the scope of the PaymentModule. This causes circular dependency at the service level.

To resolve this, you can use the @Inject decorator in combination with the forwardRef function in the service constructors. This will tell NestJS to defer the resolution of the dependency the same way the forwardRef works at the module level.

order.service.ts
@Injectable()
export class OrdersService {
  constructor(
    @Inject(forwardRef(() => PaymentService))
    private readonly paymentService: PaymentService,
  ) {}
payment.service.ts
@Injectable()
export class PaymentService {
  constructor(
    @Inject(forwardRef(() => OrdersService))
    private readonly ordersService: OrdersService,
  ) {}
}

By using the @Inject(forwardRef(() => ServiceName)), NestJS’s DI container defers the resolution of the specified service until it is needed. Now, both OrdersService and PaymentService can be instantiated without immediately needing the other to be fully resolved. This breaks the circular dependency at service level and allows NestJS to successfully inject the dependencies.

Shared Module as an Alternative Solution

Suppose, in your E-commerce application, you want to have a refund process feature where users can request refunds of their orders if they are not satisfied. This feature will involve both Order and Payment modules.

  • OrderService needs to verify an order’s eligibility for a refund (e.g. return timeframe or if it has been refunded already).
  • PaymentService needs to process the refund through any payment gateway you decide to integrate with and tell OrderService to update the order status to refunded.

This will create a circular dependency case as OrderService depends on PaymentService to process the refund and the PaymentService depends on OrderService to complete the refund process by updating the order refund status.

Instead of resolving the circular dependency that might occur with the forwardRef function, you can decouple the services depending on each other by introducing a shared module called RefundManagementModule. This module will orchestrate the refund process as follows:

refund management service
import { Injectable } from '@nestjs/common';
import { OrderService } from './order.service';
import { PaymentService } from './payment.service';

@Injectable()
export class RefundManagementService {
  constructor(
    private orderService: OrderService,
    private paymentService: PaymentService
  ) {}

  async processRefund(orderId: string) {
    const eligible = await this.orderService.checkRefundEligibility(orderId);
    if (!eligible) {
      throw new Error('Order not eligible for refund');
    }

    const refundSuccessful = await this.paymentService.processRefund(orderId);
    if (refundSuccessful) {
      await this.orderService.updateOrderStatus(orderId, 'Refunded');
    }
  }
}

Here, checkRefundEligibility checks the refund eligibility for an order. If the eligibility criteria pass, it proceeds to process refund for that order using the processRefund method from PaymentService. This service can then be exported within its module RefundManagementModule and imported in each of the OrdersService and PaymentModule for usage. With this, no two modules are depending on each other.

Tip: When writing your server applications, you should avoid circular dependency as much as possible. When you notice that you have a circular dependency, try to think of ways to refactor your application’s architecture and split up your code that does not require a lazy evaluator like the forwardRef method we used earlier. If two services within a module context have a single method that they need, you can try to move it into a shared folder to have its own provider and utility-type module.

Using Madge to Detect Circular Dependency

You can detect circular dependencies in your application by using a developer tool called Madge. Madge generates a visual graph of your module dependencies and finds circular dependencies in your code.

Install madge as a dependency using npm i madge or yarn add madge and run the command:

npx madge --circular src/main.ts

or

yarn madge --circular src/main.ts

The circular/.circular() returns an array of all modules that have circular dependencies. To learn about other methods that can be called when using this package, visit here.

When you run the command, you should have an output like this in your terminal to show you where your circular files are:

An Image showing circular dependency in our application using the madge CLI

The @Inject decorator is a fundamental component of the NestJS’s DI system. Its primary function is explicitly defining a dependency to be injected into a class usually a provider.

Conclusion

In this article, you took a deep dive into understanding circular dependency in NestJS, the types, and best practices on how to resolve them. You will find the complete source code of this tutorial here on Github.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors

Default avatar

Technical Writer


Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel