Stellarion
Quality

Circular Dependencies

Circular dependencies occur when two or more modules depend on each other, creating a cycle in the dependency graph. Module A imports Module B, which imports Module C, which imports Module A—forming a closed loop that can cause build failures, runtime errors, and architectural decay.

What They Are

A circular dependency exists when:

Module A → Module B → Module C → Module A

Or the simplest case:

Module A ⇄ Module B

These cycles violate the principle of unidirectional dependency flow and create tightly coupled code that's difficult to test, maintain, and refactor.

How Stellarion Detects Them

Stellarion uses graph-based cycle detection:

  1. Dependency Graph Construction: Analyzes imports and exports across your codebase using AST parsing
  2. Cycle Detection: Applies BFS/DFS algorithms to find cycles in the dependency graph
  3. Cycle Classification: Groups cycles by length and identifies the modules involved
  4. Impact Assessment: Evaluates which cycles are most critical based on affected code paths

The analysis uses KuzuDB for efficient graph traversal, examining:

  • Import edges (module-level dependencies)
  • Export edges (what each module provides)
  • Call edges (function-level relationships)

Severity Classification

Stellarion enforces strict standards for circular dependencies:

CountSeverityAssessment
0ExcellentClean architecture
1-3WarningNeeds attention
>3CriticalArchitectural problem

Stellarion's stance: Zero tolerance for circular dependencies in production code.

Why They're Problematic

Build and Runtime Issues

  • Module loading failures: Many bundlers and runtimes struggle with circular imports
  • Undefined values: Modules may receive undefined for values not yet initialized
  • Non-deterministic behavior: Load order becomes unpredictable

Architectural Decay

  • Hidden coupling: Modules that should be independent become intertwined
  • Refactoring resistance: Changes in one module cascade unpredictably
  • Testing complexity: Mocking and isolation become difficult

Developer Experience

  • Mental overhead: Understanding code flow requires tracking cycles
  • Onboarding friction: New developers struggle to understand the system
  • Code review complexity: Changes touch more files than expected

Common Patterns That Cause Cycles

1. Utility Module Anti-Pattern

user.ts → utils.ts → user.ts

A "utils" module that imports from modules it's supposed to serve.

2. Manager/Service Coupling

OrderService → PaymentService → OrderService

Services that call each other bidirectionally.

3. Type Sharing Gone Wrong

types.ts → models.ts → types.ts

Type definitions that depend on implementations.

How to Break Circular Dependencies

1. Dependency Inversion

Introduce an abstraction (interface) that both modules depend on:

// Before: A → B → A (cycle)

// After:
// A → Interface ← B (no cycle)
interface PaymentProcessor {
  process(order: Order): Promise<void>;
}

2. Extract Common Code

Move shared code to a new module that both can import:

// Before: A ⇄ B (cycle)

// After:
// A → Common ← B (no cycle)
// common.ts contains shared types and utilities

3. Merge Modules

If two modules are truly inseparable, they may belong together:

// Before: userAuth.ts ⇄ userProfile.ts

// After: user.ts (single module)

4. Event-Driven Architecture

Replace direct calls with events or callbacks:

// Before: OrderService.complete() calls PaymentService.charge()
//         PaymentService.refund() calls OrderService.update()

// After: Both publish/subscribe to events
eventBus.emit('order.completed', order);
eventBus.on('payment.refunded', handleRefund);

5. Lazy Loading

Defer dependency resolution to runtime:

// Instead of top-level import
// import { UserService } from './userService';

// Use dynamic import when needed
const { UserService } = await import('./userService');

When Stellarion detects circular dependencies:

  1. Immediate: Document all cycles and affected modules
  2. Short-term: Break the shortest cycles first (often quickest wins)
  3. Medium-term: Refactor using dependency inversion for complex cycles
  4. Long-term: Establish architecture rules to prevent new cycles

Prevention Strategies

  • Layered Architecture: Enforce unidirectional dependencies between layers
  • Linting Rules: Use ESLint plugins like eslint-plugin-import to detect cycles
  • CI/CD Gates: Block PRs that introduce new circular dependencies
  • Regular Audits: Run Stellarion analysis as part of your quality workflow

For the complete metrics reference, see Stellarion Quality Metrics.