Debugging Circular Dependencies in Node.js with Madge

TL;DR: A practical journey through identifying and resolving circular dependencies using the Madge tool, with visualization and refactoring strategies.

Circular dependencies in Node.js can silently degrade your application’s performance and cause unpredictable behavior. This article documents a real-world journey of identifying and resolving these issues, cutting service load time from 20+ seconds to under 12 seconds.

The Problem

The issue first surfaced when service loading time increased to 20.42 seconds, severely affecting performance and causing prolonged startup delays. The console consistently showed warnings like:

(node:54756) Warning: Accessing non-existent property 'Symbol(Symbol.toStringTag)' 
of module exports inside circular dependency

These warnings indicate circular dependencies—when modules import each other directly or indirectly, causing unpredictable behavior and performance issues.

Impact on Production

The consequences were significant:

  • Increased Service Load Time: Application took longer to start, affecting uptime and user experience
  • Server Resource Consumption: Higher CPU and memory usage during startup
  • Unreliable Behavior: Modules were sometimes only partially loaded, leading to runtime errors and inconsistent responses

Research and Analysis

Three main techniques were identified for resolution:

  1. Event-Driven Communication: Instead of direct calls, utilize event emitters to reduce direct module interactions
  2. Dependency Injection: Use dependency injection to break the cyclic import pattern
  3. Intermediate Service: Create a separate service layer to reduce direct dependencies between modules

Tool Discovery: Madge

During research, we discovered Madge, an NPM package that detects circular dependencies efficiently.

Installation

npm install -g madge

Usage

madge --circular /path/to/project

Madge can also generate visual dependency graphs, which helps understand the overall module structure.

Initial Findings

After running Madge, it detected several critical circular dependency chains:

  1. src/xxx/integration/xxx/xxx.mixin.jssrc/xxx/integration/xxx/xxx.mixin.js

  2. src/xxx/data/mongo/xxx/xxx.jssrc/xxx/data/mongo/xxx.service.js

  3. src/xxx/data/mongo/xxx.service.jssrc/xxx/data/mongo/xxx/xxx.js

Solution Implementation

After evaluating different approaches, a combination of intermediate service and modularization worked best:

1. Separated Responsibilities

Broke down larger modules into smaller, manageable components. Each module should have a single responsibility and minimal dependencies on others.

2. Intermediate Service Layer

Created new service layers to mediate between heavily interconnected modules, avoiding direct imports:

// Before: Direct circular import
// moduleA.js
const moduleB = require('./moduleB');

// moduleB.js  
const moduleA = require('./moduleA'); // Circular!

// After: Intermediate service
// sharedService.js
class SharedService {
  // Shared functionality extracted here
}

// moduleA.js
const sharedService = require('./sharedService');

// moduleB.js
const sharedService = require('./sharedService'); // No more circular!

3. Modularization

Reorganized code to follow a clear structure, minimizing cross-references between modules:

src/
├── xxx/
│   ├── api/           # API layer - depends on services
│   ├── services/      # Business logic - depends on data
│   ├── data/          # Data access - no upstream deps
│   └── utils/         # Shared utilities - minimal deps

Validation

After implementing the fixes, Madge confirmed all circular dependencies were resolved:

$ madge --circular src/
 No circular dependency found!

Outcome and Improvement

The results were impressive:

MetricBeforeAfter
Service Load Time20.42 seconds~12 seconds
Circular Dependencies5 chains0
Application StabilityIntermittent errorsStable
  • 41% reduction in startup time
  • Zero circular dependency warnings
  • Improved reliability in production

Best Practices for Prevention

1. Integrate Madge into CI/CD

Add circular dependency checks to your pipeline:

# .github/workflows/ci.yml
- name: Check circular dependencies
  run: |
    npm install -g madge
    madge --circular src/ && echo "No circular deps found" || exit 1

2. Follow Dependency Direction Rules

Establish clear dependency directions:

  • API layer → Service layer → Data layer
  • Never allow upstream dependencies
  • Utilities should have zero business logic dependencies

3. Use Dependency Injection

Instead of importing dependencies directly, inject them:

// Instead of this:
class UserService {
  constructor() {
    this.emailService = require('./emailService');
  }
}

// Do this:
class UserService {
  constructor(emailService) {
    this.emailService = emailService;
  }
}

4. Regular Code Reviews

During code reviews, specifically check for:

  • New require/import statements that might create cycles
  • Modules that seem to need “everything”
  • Utility files that grow too large

Conclusion

Circular dependencies are a common but often overlooked source of performance issues in Node.js applications. With tools like Madge and disciplined architectural practices, you can detect and resolve these issues systematically.

The key takeaways:

  1. Use Madge regularly to detect circular dependencies early
  2. Design modules with clear, unidirectional dependency flows
  3. Extract shared functionality into intermediate services
  4. Integrate checks into your CI/CD pipeline

The 40%+ improvement in startup time demonstrates that these aren’t just theoretical concerns—they have real, measurable impact on your application’s performance.

Acknowledgements
  • Charu — Toolkit study and finalisation