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.
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.
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"
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"