Circular Dependencies in Nestjs: What is it about?

TL;DR: Circular dependencies can be harmful to a software system, but there are tools, techniques, and best practises for dealing with them.

In Software Development, we build modules that rely on each other to function properly. These may include various aspects of the codebase, including code libraries, external APIs, system components, and more. These kinds of services or modules are called Dependencies. We have types of Dependencies, namely:

  • Compile-time dependencies: These are dependencies that are required for a program to be compiled or built.

  • Runtime dependencies: These are dependencies that are required for a program to run successfully.

  • Transitive dependencies: These are dependencies that are indirectly required by a program due to other dependencies. For example, if a program depends on library A, and library A depends on library B, then library B is a transitive dependency of the program.

  • Circular dependencies: These are dependencies that form a circular chain, where one component depends on another component that, in turn, depends on the first component. This can create issues and make it difficult to understand and maintain the codebase.

It is important to note that understanding and managing dependencies are an essential part of software development, as they can impact the stability, reliability, and maintainability of a system. By identifying and addressing dependencies, developers can improve the quality and effectiveness of their software. Our focus in this article will be on what Circular Dependencies are, their causes, and effects, and how best to resolve them. Having said that, let's dive into the business of today. We'll start by defining Circular Dependencies.

Definition of Circular Dependencies

In programming, a circular dependency occurs when two or more modules (or classes), directly or indirectly depend on one other.

Here's a scenario to better understand the meaning:

Let's assume we have two modules, A and B. Module A relies on a service provided by Module B which relies on a service provided by Module A. We can see that a loop is created which goes back and forth between them. It is important to have a good understanding of Circular Dependency as long as you plan on building systems to stand the test of time. I'll be giving you a few of the reasons:

  • It helps to improve your system stability: We get to identify and resolve issues relating to circular dependencies since it leads to errors, bugs, and other issues thereby increasing the stability of our applications.

  • Enabling better software design: By understanding circular dependencies, we get to improve our software architectural design as developers.

  • Improve code quality with time.

In the next section, we will be looking at factors that cause this situation.

Cause of Circular Dependencies

Circular dependencies can arise from a variety of causes, including:

  • Poor system design: Circular dependencies can be a symptom of poor system design, where components or modules are not properly organized or separated. This can lead to circular chains of dependencies that make it difficult to maintain or update the system.

  • Overly complex code: When code becomes overly complex, it can be difficult to untangle circular dependencies. This can occur when code is written in a monolithic style, with many interdependent components, rather than using a modular approach that separates concerns.

  • Lack of communication or coordination: Circular dependencies can also arise when different teams or developers are working on different parts of a system and fail to communicate or coordinate properly. This can lead to components being developed independently and then integrated into the system, creating circular chains of dependencies.

  • Unanticipated changes: As a system evolves over time, changes to one component can have unintended consequences on other components that depend on it. These unintended consequences can create circular dependencies that were not present in the original system design.

  • Third-party dependencies: Circular dependencies can also arise from third-party dependencies, such as libraries or APIs, that have circular dependencies themselves. This can create a cascading effect that can be difficult to resolve.

By understanding the causes of circular dependencies, developers can take steps to prevent them from occurring in the first place, as well as identify and address them when they do occur.

Examples of Circular Dependencies

Here are examples of scenarios where circular dependencies can occur:

  • It can happen between providers: Providers are Javascript classes that provide various functionalities to an application. Services like database access, logging, and handling business logic can be defined in a provider. Circular dependencies can occur in providers in a situation where provider A depends on provider B, and provider B depends on provider A, then a circular dependency has been created.

  • It can happen between services and controllers

  • It can happen between modules

Effects and how to detect Circular Dependencies in applications

Having circular dependencies in an application can result in any or more of one of the following:

  • It reduces the maintainability of our system.

  • It increases the complexity of our code: Since modules that depend on each other lead to being loaded in a particular order to function, it adds unnecessary complexity.

  • It reduces performance and the ability to test our components.

So can we detect circular dependencies in our applications? There are a few techniques that can be used in detecting. Here they are:

  • Code review: Reviewing the codebase for dependencies between components can help identify circular dependencies. Code reviews can also help identify other issues, such as overly complex code or poor system design.

  • Static analysis tools: There are many static analysis tools available that can scan code for circular dependencies and other issues. For example, Dependency-cruiser is an open-source tool that analyzes dependencies between files and modules in a codebase and can detect circular dependencies, as well as other issues such as missing or unused dependencies. These tools can be integrated into a continuous integration or continuous delivery pipeline to automatically detect issues before they cause problems.

  • Runtime analysis: It is also possible to use runtime analysis tools to detect circular dependencies at runtime. For example, Madge is an open-source tool that analyzes dependencies between files and modules in a codebase and can detect circular dependencies, as well as other issues such as missing or unused dependencies. These tools can monitor the interactions between components and identify circular chains of dependencies.

So having seen the definition, causes, effects, and detecting techniques, how do we resolve the circular dependencies in our applications?

How to resolve Circular Dependencies

Resolving circular dependencies can be a challenge especially when confused about what to do. We'll be looking at several tools, techniques, and best practices that can help.

  • Refactor code: It may end up in a situation where you have to refactor your code to fix the issue

  • Using analytic tools: As mentioned earlier, using runtime and static analysis tools can help developers to better detect issues and resolve them ahead of time.

  • Re-architect your system: Circular dependencies may sometimes require re-architecting the system in order to be resolved. To better manage dependencies, this may entail rethinking the system's design or rearranging the components.

  • Try to break up large modules in the system: Dividing large modules into smaller, more focused modules may be advantageous if there are circular dependencies between them. Large modules can be divided into smaller ones to help manage dependencies and prevent circular dependencies.

Conclusion

Circular dependencies can harm a software system in several ways, including making it more difficult to maintain, more complex, harder to test, and less performant. Developers can utilize technologies like dependency injection, split up huge modules, employ a layered design, and use static and runtime analysis tools to handle circular dependencies. In some circumstances, refactoring code and redesigning the system architecture may also be required. Developers can construct software systems that are simpler to manage, test, and modify by addressing circular dependencies.