Avoiding Circular Dependencies

Architecture rules for preventing circular dependencies between modules

🚫 Avoiding Circular Dependencies

The Problem

Circular dependency detected:

launch-station-lib → space-map-lib → core-angular → gograb-space → launch-station-lib

This happens when modules import from each other, creating a cycle that breaks the build.

❌ What NOT To Do

Don't Import Components from Other Modules

// ❌ BAD: Creates circular dependency
// In launch-station-lib/src/lib/launch-station.module.ts
routes: [
  {
    path: ':universeId',
    loadComponent: () => import('@space-map-lib')  // ❌ BAD!
      .then(m => m.UniverseDeploymentComponent)
  }
]

Why this fails:

✅ Correct Approaches

1. Use Local Components (Preferred)

// ✅ GOOD: Import from same library
// In launch-station-lib/src/lib/launch-station.module.ts
routes: [
  {
    path: ':universeId',
    loadComponent: () => import('./components/universe-deployment/universe-deployment.component')
      .then(m => m.UniverseDeploymentComponent)
  }
]

Benefits:

2. Create Shared Component Library

If components are truly shared, create a dedicated library:

// Create: libs/shared-deployment-components/
// This library is imported BY both launch-station and space-map

// In launch-station-lib:
loadComponent: () => import('@shared-deployment-components')
  .then(m => m.UniverseDeploymentComponent)

// In space-map-lib:
loadComponent: () => import('@shared-deployment-components')
  .then(m => m.UniverseDeploymentComponent)

Dependency hierarchy:

shared-deployment-components (bottom)
        ↑
  ┌─────┴─────┐
  ↑           ↑
launch-station  space-map (top)

No circular dependency! ✅

3. Use Embeddable Components (For Cross-Module UI)

If you need to use another module's UI:

// In space-map-lib manifest:
embeddableComponents: [
  {
    id: 'universe-deployment-widget',
    componentType: UniverseDeploymentComponent,
    // ...
  }
]

// In launch-station-lib:
// Use the embeddable component via module registry
const registry = inject(ModuleRegistryService);
const component = registry.getEmbeddableComponent('space-map', 'universe-deployment-widget');

🏗️ Library Architecture Rules

Rule 1: Clear Dependency Hierarchy

Core Libraries (bottom layer)
    ↓
Platform Tools (middle layer)  
    ↓
Feature Modules (top layer)
    ↓
Application (gograb-space)

Examples:

✅ launch-station-lib → core-angular (OK - top to bottom)
✅ space-map-lib → core-angular (OK - top to bottom)
❌ launch-station-lib → space-map-lib (BAD - lateral import)
❌ space-map-lib → launch-station-lib (BAD - lateral import)

Rule 2: Platform Tools Are Independent

Platform tools should NOT import from each other:

They CAN import from:

Rule 3: Shared Code Goes Down, Not Sideways

If two platform tools need the same component:

❌ BAD (sideways):
launch-station → space-map → [component]

✅ GOOD (extract down):
        shared-lib
           ↑
    ┌──────┴──────┐
launch-station  space-map

📋 Checklist Before Adding Imports

Before adding import '@some-lib':

  1. ✅ Is it a core library (@libs-core/*)? → OK
  2. ✅ Is it a local component (./components/...)? → OK
  3. ❌ Is it another platform tool? → STOP! Create shared lib or use local component
  4. ❌ Does it create a cycle with gograb-space? → STOP! Refactor

🔍 How to Detect Circular Dependencies

During Development

# Build specific library
nx build launch-station-lib

# Look for circular dependency warnings
# Error message will show the cycle

In CI/CD

# Lint for circular dependencies
nx run-many -t lint

# Generate dependency graph
nx graph

🛠️ How to Fix Existing Circular Dependencies

Step 1: Identify the Cycle

A → B → C → A

Step 2: Break the Cycle

Option A: Move shared code to lower layer

shared-lib (new)
    ↑
 ┌──┴──┐
 A     B     C (no longer depends on A)

Option B: Remove unnecessary import

A → B → C (removed dependency on A)

Option C: Use dependency injection

// Instead of direct import, use DI
constructor(private moduleRegistry: ModuleRegistryService) {
  // Get component dynamically
  const component = this.moduleRegistry.getEmbeddableComponent('module-id', 'component-id');
}

📚 Real Example: Launch Station Fix

Before (Broken)

// launch-station-lib/src/lib/launch-station.module.ts
routes: [
  {
    path: ':universeId',
    loadComponent: () => import('@space-map-lib')  // ❌ Circular!
      .then(m => m.UniverseDeploymentComponent)
  }
]

Cycle:

launch-station-lib → space-map-lib → core-angular → gograb-space → launch-station-lib

After (Fixed)

// launch-station-lib/src/lib/launch-station.module.ts
routes: [
  {
    path: '',
    loadComponent: () => import('./components/launch-dashboard/launch-dashboard.component')
      .then(m => m.LaunchDashboardComponent)
  }
  /* TODO: Create local component
  {
    path: ':universeId',
    loadComponent: () => import('./components/universe-deployment/universe-deployment.component')
      .then(m => m.UniverseDeploymentComponent)  // ✅ Local component
  }
  */
]

No cycle! ✅

🎯 Best Practices

DO:

DON'T:

🚦 Quick Decision Tree

Need component from another module?
    ↓
Is it in a core library?
    ├─ YES → Import it ✅
    └─ NO → ↓
Is it another platform tool?
    ├─ YES → DON'T import! Create local or shared lib ❌
    └─ NO → ↓
Is it truly shared by 3+ modules?
    ├─ YES → Create shared library ✅
    └─ NO → Create local component ✅

📖 Related Documentation


Status: 🎯 Critical Architecture Rule
Last Updated: November 1, 2025
Severity: Breaking (build fails)
Prevention: Always check imports before committing