Back to homepage

What is a Circular dependency

Resource A is dependent on Resource B, and vice versa.

A common problem encountered in any language or framework where modules are tightly coupled is a circular dependency. This occurs when Resource A depends on Resource B, and Resource B simultaneously depends on Resource A.

Circular dependency

In the example below, a function named hello imports world from a file named foo.js:

//src/components/bar.js
import { world } from "../foo";

const hello = () => {
	return "Hello" + world();
}

export { hello };

At the same time, another function named world imports hello from a file named bar.js:

//src/components/foo.js
import { hello } from "../bar";

const world = () => {
	return "Hello" + world();
}

export { world };

In other words, these two functions are dependent on each other in a circular loop. If you run this code in a browser, you will likely encounter a maximum call stack size exceeded error, indicating an infinite recursive loop.

What to avoid

Ex:

 - register
   • utils.js
 - signin
   • import utils from '../register/utils'

In this scenario, we have two features: register and signin. The register module contains utility functions that are being imported by the signin module.

This approach is likely to lead to a circular dependency eventually because the "signin" feature relies on functionality nested within another specific feature module. Additionally, any future modifications to "register" might not be suitable for the "signin" module, requiring further brittle modifications as a result.

How to fix it

 - registger
   • import utils from '../shared/utils'
 - signin
   • import utils from '../shared/utils'
 - shared
   • utils

As shown above, the utilities are moved into a dedicated shared folder. This prevents circular dependencies from occurring in the first place.

Both modules use the shared utilities, but the shared folder does not import anything from the register or signin features. As long as the shared utilities remain decoupled and isolated, the architecture remains clean.

A Real-World Scenario

Consider an online store where a Product module handles product details and a Cart module manages shopping cart functionality and rendering.

Whenever a new product is added, the Product module updates the Cart module. In turn, the Cart module updates functionality such as total amounts and the product list. If the Cart module then needs to pull product information back from the Product module to display it, a circle is formed.

// Product.js
const Cart = require('./Cart');

function Product(name, price) {
	this.name = name;
	this.price = price;
}

Product.prototype.addToCart = function() {
	Cart.addProduct(this); // Adding this product to the Cart
}

module.exports = Product;

// Cart.js
const Product = require('./Product');

let productsInCart = [];

const Cart = {
	addProduct: function(product) {
		productsInCart.push(product);
		console.log(`${product.name} added to cart`);
	},
	displayCart: function() {
		return productsInCart.map(p => `${p.name}: $${p.price}`).join(', ');
	}
}

module.exports = Cart;

CAKE®STACK

Rolling With The Dough