Breaking Cycles
Breaking Cycles
Section titled “Breaking Cycles”Circular dependencies (cycles) are one of the most common architectural issues. This guide shows you how to identify and fix them.
What Are Cycles?
Section titled “What Are Cycles?”A cycle occurs when modules depend on each other in a circular way:
A imports B → B imports C → C imports AThis creates a Strongly Connected Component (SCC) where all modules are mutually dependent.
Why Cycles Are Bad
Section titled “Why Cycles Are Bad”- Harder to understand - Cannot reason about modules in isolation
- Harder to test - Cannot test modules independently
- Blocks tree-shaking - Bundlers cannot eliminate dead code
- Prevents optimization - Compilers cannot optimize
- Increases coupling - Changes propagate unpredictably
Goal: Zero cycles. Every module should have clear dependencies flowing in one direction.
Detecting Cycles
Section titled “Detecting Cycles”Run SCC Analysis
Section titled “Run SCC Analysis”arxo analyze --metric sccOutput:
📊 SCC Metrics: scc.component_count: 75 (183 total nodes) scc.max_cycle_size: 8 ⚠️ Large cycle! scc.total_nodes_in_cycles: 24Interpretation:
scc.component_count= 75 means there are 75 SCCs (should be 183 for no cycles)scc.max_cycle_size= 8 means the largest cycle has 8 modules- 24 modules are stuck in cycles
Get Cycle Details
Section titled “Get Cycle Details”arxo analyze --format json --output report.json
# Extract SCC detailsjq '.results[] | select(.id=="scc") | .evidence' report.jsonExample Evidence:
{ "evidence": [ { "type": "cycle", "severity": "high", "nodes": [ "src/services/auth.ts", "src/services/user.ts", "src/services/session.ts" ], "message": "Circular dependency involving 3 modules" } ]}Common Cycle Patterns
Section titled “Common Cycle Patterns”Pattern 1: Mutual Dependencies
Section titled “Pattern 1: Mutual Dependencies”Problem:
import { User } from './user';
export class Auth { validateUser(user: User) { }}
// services/user.tsimport { Auth } from './auth';
export class User { auth: Auth;}Solution 1: Extract Interface
export interface IAuth { validateUser(user: User): boolean;}
// services/user.tsimport { IAuth } from '../interfaces/IAuth';
export class User { auth: IAuth; // Depend on interface}
// services/auth.tsimport { User } from './user';import { IAuth } from '../interfaces/IAuth';
export class Auth implements IAuth { validateUser(user: User) { }}Solution 2: Dependency Injection
export class User { constructor(private auth: any) { } // Inject at runtime}
// services/auth.tsimport { User } from './user';
export class Auth { validateUser(user: User) { }}
// index.tsconst auth = new Auth();const user = new User(auth);Pattern 2: Type Cycles
Section titled “Pattern 2: Type Cycles”Problem:
import { Post } from './Post';
export interface User { posts: Post[];}
// models/Post.tsimport { User } from './User';
export interface Post { author: User;}Solution: Extract Type Definitions
import { Post } from './Post';
export interface User { posts: Post[];}
// types/Post.tsimport { User } from './User';
export interface Post { author: User;}
// models/User.tsimport { User } from '../types/User';
export class UserModel implements User { posts = [];}
// models/Post.tsimport { Post } from '../types/Post';
export class PostModel implements Post { author!: User;}Types can have cycles (they’re compile-time only), but implementations should not.
Pattern 3: Utility Cycles
Section titled “Pattern 3: Utility Cycles”Problem:
import { formatNumber } from './number';
export function formatString(s: string, n: number) { return `${s}: ${formatNumber(n)}`;}
// utils/number.tsimport { formatString } from './string';
export function formatNumber(n: number) { return formatString('Number', n); // Cycle!}Solution: Extract Shared Utility
export function formatString(s: string, n: string) { return `${s}: ${n}`;}
// utils/number.tsexport function formatNumber(n: number) { return n.toFixed(2);}
// utils/composite.tsimport { formatString } from './string';import { formatNumber } from './number';
export function formatNumberWithLabel(label: string, n: number) { return formatString(label, formatNumber(n));}Pattern 4: Parent-Child Cycles
Section titled “Pattern 4: Parent-Child Cycles”Problem:
import { Child } from './Child';
export function Parent() { return <Child />;}
// components/Child.tsximport { Parent } from './Parent';
export function Child() { // Somehow uses Parent}Solution: Inversion of Control
export function Child({ onAction }: { onAction: () => void }) { return <button onClick={onAction}>Click</button>;}
// components/Parent.tsximport { Child } from './Child';
export function Parent() { const handleAction = () => { /* ... */ }; return <Child onAction={handleAction} />;}Step-by-Step Cycle Breaking
Section titled “Step-by-Step Cycle Breaking”1. Identify the Cycle
Section titled “1. Identify the Cycle”# Get JSON reportarxo analyze --format json --output report.json
# Find largest cyclejq '.results[] | select(.id=="scc") | .evidence[] | select(.severity=="high")' report.jsonOutput:
{ "nodes": ["A.ts", "B.ts", "C.ts"], "size": 3}2. Visualize Dependencies
Section titled “2. Visualize Dependencies”Create a diagram of the cycle:
A.ts → B.ts ↑ ↓ └── C.ts3. Find the Weakest Link
Section titled “3. Find the Weakest Link”Ask:
- Which dependency is least essential?
- Which module could work without the other?
- Which import is only for types?
4. Break the Cycle
Section titled “4. Break the Cycle”Apply one of these strategies:
Strategy 1: Extract Shared Code
- Move common code to a new module both can import
Strategy 2: Dependency Injection
- Pass dependencies at runtime instead of importing
Strategy 3: Event System
- Use events/callbacks instead of direct calls
Strategy 4: Interface Segregation
- Depend on interfaces, not implementations
5. Verify Fix
Section titled “5. Verify Fix”arxo analyze --metric scc
# Should show:# scc.max_cycle_size: 0 ✓Preventing Future Cycles
Section titled “Preventing Future Cycles”1. Policy Enforcement
Section titled “1. Policy Enforcement”Add to .arxo.yaml:
policy: invariants: - metric: scc.max_cycle_size op: "==" value: 0 message: "No circular dependencies allowed"Run in CI:
arxo analyze --preset ci --fail-fast2. Pre-commit Hook
Section titled “2. Pre-commit Hook”#!/bin/basharxo analyze --quick --metric scc --quietif [ $? -ne 0 ]; then echo "❌ Circular dependency detected!" exit 1fi3. IDE Integration
Section titled “3. IDE Integration”VSCode Extension:
- Install the VSCode extension
- Get warnings on cycles as you code
MCP with AI Assistant:
- Install the MCP server
- Ask your AI: “Check for circular dependencies”
- Uses
check_cyclesfor instant feedback (see MCP Workflows)
Example conversation:
You: "Before I commit, check for cycles"AI: [Runs check_cycles] "✓ No circular dependencies detected"4. Code Review Checklist
Section titled “4. Code Review Checklist”- Does this PR introduce new imports?
- Could these imports create a cycle?
- Run
arxo analyze --quickbefore review
Real-World Examples
Section titled “Real-World Examples”Example 1: Redux Store Cycles
Section titled “Example 1: Redux Store Cycles”Before:
import { updateSession } from './session';
// store/session.tsimport { clearUser } from './user';After:
export const userSlice = createSlice({ name: 'user', reducers: { clearUser }});
// store/session.tsexport const sessionSlice = createSlice({ name: 'session', reducers: { updateSession }});
// store/index.tsimport { userSlice } from './user';import { sessionSlice } from './session';
// Combine slices (no cycles)export const store = configureStore({ reducer: { user: userSlice.reducer, session: sessionSlice.reducer }});Example 2: React Component Cycles
Section titled “Example 2: React Component Cycles”Before:
import { UserMenu } from './UserMenu';
// UserMenu.tsximport { Header } from './Header'; // Cycle!After:
export function UserMenu() { // No import of Header}
// Header.tsximport { UserMenu } from './UserMenu';
export function Header() { return ( <header> <UserMenu /> </header> );}Troubleshooting
Section titled “Troubleshooting””I Fixed the Cycle but SCC Still Reports It”
Section titled “”I Fixed the Cycle but SCC Still Reports It””Cause: Cache not cleared
Solution:
arxo cache cleararxo analyze --metric scc“Cycle Involves 20+ Files”
Section titled ““Cycle Involves 20+ Files””Approach:
- Break into smaller pieces
- Fix one pair at a time
- Use
--only-changedto verify progress
”Types Must Reference Each Other”
Section titled “”Types Must Reference Each Other””Solution: Types can have cycles (they’re compile-time only)
Policy:
import_graph: exclude: - "**/types/**" # Exclude type-only files from cycle checksTools for Cycle Detection
Section titled “Tools for Cycle Detection”Madge (JavaScript)
Section titled “Madge (JavaScript)”npm install -g madgemadge --circular --extensions ts,tsx src/Graphviz Visualization
Section titled “Graphviz Visualization”arxo analyze --format json | \ jq '.graphs.import_graph' | \ python to_dot.py | \ dot -Tpng -o graph.pngNext Steps
Section titled “Next Steps”- SCC Metric - Understanding cycle metrics
- Policy Examples - Enforce no-cycle policy
- CI Integration - Prevent cycles in CI
- MCP Workflows - Real-time cycle detection with AI
- Troubleshooting - Common issues