Back to Articles

A Simple Guide to Solving Circular Dependencies

Published on June 29, 20256 min read
A Simple Guide to Solving Circular Dependencies

A Simple Guide to Solving Circular Dependencies


Circular dependencies are like that one group project where everyone depends on everyone else to start—and nothing ever gets done.


In coding, it’s worse: your app might crash, hang, or behave in ways that’ll make you question your career choices.


Today, I’ll walk you through what circular dependencies are, how to spot them, and how to fix them—with a simple JavaScript example.


🧩 What is a Circular Dependency?


A circular dependency happens when two or more files/modules/classes depend on each other directly or indirectly. That creates a loop.


Imagine this:


- A depends on B

- B depends on C

- C depends on A


Boom. That’s a circle. And JavaScript doesn’t like it.



🤯 The Problem in Real Life (Simple Version)


Let’s say you’re building an app with the following modules:


- Logger.js – Logs messages

- Auth.js – Handles user authentication

- Api.js – Calls external APIs


Here’s the dependency loop we accidentally create:


// Logger.js
import Auth from './Auth';

export function log(message) {
  const user = Auth.getCurrentUser();
  console.log(`[${user}]: ${message}`);
}

// Auth.js
import Api from './Api';
import { log } from './Logger';

export const Auth = {
  getCurrentUser() {
    return "JaneDoe";
  },
  login(username, password) {
    log("Logging in...");
    return Api.postLogin(username, password);
  }
};

// Api.js
import { log } from './Logger';

export const Api = {
  postLogin(username, password) {
    log("Calling /login API");
    return Promise.resolve({ token: "123" });
  }
};

This creates:


Logger → Auth → Api → Logger

Oops. We’re in a circular dependency trap.


---


🔍 How to Spot a Circular Dependency


- 🔁 App crashes on startup

- 🔄 Stack overflow errors

- 🚫 undefined values when importing modules

- ⚠️ ESLint or IDE warnings about circular imports



✅ Fixing It the Right Way


Let’s solve it using Dependency Injection.


---


🛠️ Step-by-Step Fix


Instead of Logger depending on Auth, let’s inject the user context into it.


1. Make Logger self-contained


// Logger.js
export function createLogger(getUser) {
  return function log(message) {
    const user = getUser();
    console.log(`[${user}]: ${message}`);
  };
}

2. Use it in Auth, with no imports in reverse


// Auth.js
import { Api } from './Api';
import { createLogger } from './Logger';

let currentUser = "JaneDoe";

const log = createLogger(() => currentUser);

export const Auth = {
  getCurrentUser() {
    return currentUser;
  },
  login(username, password) {
    log("Logging in...");
    return Api.postLogin(username, password);
  }
};

3. Api stays clean


// Api.js
import { createLogger } from './Logger';

const log = createLogger(() => "System");

export const Api = {
  postLogin(username, password) {
    log("Calling /login API");
    return Promise.resolve({ token: "123" });
  }
};

✅ No more import circles.

💡 Each module is clean, testable, and standalone.



🧱 General Fix Strategies


- Dependency Injection – Pass dependencies instead of importing them inside

- Split shared logic – Move common functions to a lower-level utility module

- Lazy imports – Use import() only when needed

- Interfaces/Contracts – Code to interfaces, not implementations

- Mediator pattern – Use a separate orchestrator for coordination



🧭 Best Practices to Prevent It


- Keep modules focused on one job (Single Responsibility Principle)

- Avoid deep chains of imports

- Watch for "utils" that do too much

- Use linters like madge or eslint-plugin-import to catch cycles



🧘 Final Thoughts


Circular dependencies are sneaky but fixable. If you spot one, don’t patch over it—dig into why the loop happened. It usually signals poor separation of concerns or overly entangled modules.


👉 Break the cycle, clean up the architecture, and your codebase will thank you later.