Skip to main content

No circular module dependencies

ab-002150 · code-quality-essentials.dependencies.no-circular
Severity: highactive

Why it matters

Circular module dependencies cause subtle initialization-order bugs that appear only in specific import sequences — the kind that works on the developer's machine and fails in a bundled production build or a Jest environment with a different module evaluation order. ISO 25010:2011 §6.5.1 classifies this as a structural maintainability defect. Circular dependencies also prevent effective tree-shaking: bundlers cannot safely eliminate one half of a cycle, so both modules ship to the client even when only one is needed.

Severity rationale

High because circular imports introduce initialization-order bugs that are environment-specific and notoriously hard to diagnose, and they prevent the bundler from tree-shaking either module.

Remediation

Detect cycles with dpdm or madge, then break them by extracting the shared dependency into a third module:

npx dpdm --circular --tree false src/**/*.ts
# or
npx madge --circular src/

Breaking the cycle:

Before:
  utils/format.ts → features/user.ts → utils/format.ts  (circular)

After:
  types/user.types.ts  (new shared module)
  utils/format.ts → types/user.types.ts
  features/user.ts → types/user.types.ts

Add to CI to prevent regressions:

npx madge --circular --extensions ts src/ && echo "No circular deps"

Detection

  • ID: code-quality-essentials.dependencies.no-circular

  • Severity: high

  • What to look for: Circular dependencies occur when module A imports from module B, and module B (directly or indirectly) imports from module A. Look for this pattern in src/ by examining import statements across sibling directories. Warning signs: utility files that import from feature modules, feature modules that import from each other bidirectionally, index files that re-export from multiple directories that also import from each other. Check package.json for dpdm or madge in devDependencies — these tools detect circular dependencies automatically. Also check if the bundler (webpack, Vite) has reported circular dependency warnings. Circular dependencies cause subtle initialization order bugs and make tree-shaking less effective.

  • Pass criteria: Enumerate all import chains. No circular dependencies detected, OR dpdm/madge runs in CI and reports no cycles, OR any tolerated cycles are documented with justification.

  • Fail criteria: Visible circular import patterns, OR circular dependency tool reports cycles without documented justification.

  • Skip (N/A) when: Project is too small to have import cycles (fewer than 10 modules).

  • Detail on fail: "Potential circular dependency detected: e.g., src/utils/A.ts imports from src/features/B.ts which imports from src/utils/A.ts"

  • Remediation: To detect cycles, run:

    npx dpdm --circular --tree false src/**/*.ts
    # or
    npx madge --circular src/
    

    To break a cycle, extract the shared dependency into a third module:

    Before:
      utils/format.ts → features/user.ts → utils/format.ts  (circular)
    
    After:
      types/user.types.ts  (shared)
      utils/format.ts → types/user.types.ts
      features/user.ts → types/user.types.ts
    

    Add madge to CI to prevent regressions:

    npx madge --circular --extensions ts src/ && echo "No circular deps"
    

External references

Taxons

History