Skip to content
Arxo Arxo

Breaking Cycles

Circular dependencies are one of the fastest ways to accumulate structural debt. This guide shows a practical workflow to reduce cycles using SCC outputs.

For SCC metric details, see Circular Dependencies.

Terminal window
arxo analyze --metric scc --format json > report.json

Inspect top module-level signals:

Terminal window
jq '.results[] | select(.id=="scc") | .data[] | select(.key|test("^scc\\.(cycle_count|max_cycle_size|total_nodes_in_cycles)$"))' report.json

Prioritize if any of these are non-zero.

Use cycle microscope payload:

Terminal window
jq '.ui_schemas.scc.scc_details[] | {cycle_id, size:(.nodes|length), nodes, witness_path, risk_score}' report.json

Start with either:

  • largest cycle
  • highest risk_score
  • cycle containing high-churn files (if use_git_history: true)

List module-level cut candidates:

Terminal window
jq '.results[] | select(.id=="scc") | .data[] | select(.key=="scc.cut_candidates")' report.json

For function-level call cycles, inspect:

Terminal window
jq '.ui_schemas.scc.issues.categories.cycle_cut_candidates.critical' report.json

Pick lower-risk candidates first (lower call usage or simpler dependency to invert).

Use one of these patterns:

  1. Extract shared code into a third module.
  2. Introduce an interface/abstraction layer.
  3. Use dependency injection instead of direct construction.
  4. Move shared type contracts to a separate definitions module.

See Refactoring Patterns for concrete examples.

Re-run SCC and compare:

Terminal window
arxo analyze --metric scc

Then enforce a policy gate so cycles do not regress:

metrics:
- id: scc
policy:
invariants:
- metric: scc.cycle_count
op: "=="
value: 0
- metric: scc.max_cycle_size
op: "=="
value: 0

For legacy codebases, gate on non-regression first, then tighten thresholds over time.

policy:
baseline:
mode: git
ref: origin/main
invariants:
- metric: scc.max_cycle_size
op: "<="
baseline: true
- metric: scc.total_nodes_in_cycles
op: "<="
baseline: true

This prevents new cycle debt while existing debt is being paid down incrementally.

  • Fixing only one edge in a large cycle without re-running analysis.
  • Breaking imports but keeping circular call paths.
  • Enabling strict zero-cycle policy too early on legacy repos.
  • Ignoring high-churn files in cycle triage.