Skip to content
Arxo Arxo

Circular Dependencies

The Circular Dependencies metric finds import cycles — modules that depend on each other directly or indirectly, forming a loop. Cycles are the most common structural problem in growing codebases: they create tight coupling, complicate testing, and make refactoring painful.

A cycle exists when module A imports B, B imports C, and C imports A (or any longer chain that loops back).

MetricWhat it tells youIdeal
scc.max_cycle_sizeSize of the largest cycle (in modules)0 (no cycles)
scc.total_nodes_in_cyclesHow many modules are stuck in cycles0
scc.component_countNumber of independent groups (higher = healthier)Equal to total module count

When call graph analysis is enabled, cycles are also detected at the function level:

MetricWhat it tells youIdeal
scc.function.max_cycle_sizeLargest function-level cycle0
scc.function.total_nodes_in_cyclesFunctions stuck in call loops0
scc.function.call_massHow much runtime traffic flows through cycles0
scc.function.cycle_cut_candidates_countSuggested edges to break

In JSON/UI output you also get:

  • Cycle details: Which files are in each cycle, internal edges, and boundary connections
  • Cycle-cut candidates: The cheapest edges to remove to break each cycle
  • Hotspot data (with use_git_history: true): Which files in cycles are changed most often — fix those first

Circular dependencies:

  • Block independent testing — you can’t test one module without pulling in the whole cycle
  • Prevent tree-shaking — bundlers can’t remove unused code inside cycles
  • Slow down builds — compilers can’t parallelize modules in a cycle
  • Make refactoring risky — changing one file in a cycle can break all the others
  • Hide behind indirection — cycles through 10+ modules are invisible without tooling
GuideWhat you’ll learn
InterpretationHow to read cycle results, set thresholds, and use cycle-cut candidates
Cycle MicroscopePer-cycle details, node table, boundary edges, and refactor priority
Function-Level CyclesDetect call-graph cycles and use function-level cycle-cut candidates
Git History IntegrationEnable churn and hotspot scores to prioritize which files to fix first
Fixing CyclesStep-by-step process to identify and break cycles
Refactoring PatternsExtract shared modules, dependency injection, types files
PolicyEnforce cycle limits in CI (strict, pragmatic, or gradual)
PerformanceBenchmarks and comparison with other tools
CLI and IntegrationCLI usage, pre-commit hooks, GitHub Actions, IDE
Cross-Metric ImpactHow fixing cycles improves Layer Structure, Change Impact, and other metrics
Language-Specific PatternsHow cycles show up in TypeScript, Python, Rust, Java, Go, and how to fix them
  1. Aim for zero cycles in new code
  2. Fix small cycles first — 2-3 module cycles are quick wins
  3. Use cycle-cut candidates — start with the edge that has the lowest call count
  4. Enable in CI — prevent new cycles from being introduced
  5. Track over time — use baseline comparison to measure progress
  6. Fix high-churn files first — prioritize files that change often (enable git history)
  7. Document exceptions — if a cycle must stay, explain why

Cycle detection uses Tarjan’s algorithm (1972), which finds all cycles in linear time. In the Prettier codebase (4,883 files), Arxo detected a 56-module cycle that other tools missed entirely — see Performance for details.

Research shows that circular dependencies increase maintenance costs by 40-60% and correlate with higher defect rates.