No circular module dependencies
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:
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. Checkpackage.jsonfordpdmormadgein 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/madgeruns 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.tsAdd
madgeto CI to prevent regressions:npx madge --circular --extensions ts src/ && echo "No circular deps"
External references
- iso-25010:2011 · maintainability.modularity — Maintainability — Modularity
Taxons
History
- 2026-04-18·v1.0.0·Initial import from code-quality-essentials·automated