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:
launch-station-libimports fromspace-map-libspace-map-libimports fromcore-angularcore-angularis used bygograb-spacegograb-spaceimportslaunch-station-lib- CIRCULAR! 🔄
✅ 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:
- No circular dependencies
- Clear ownership
- Better encapsulation
- Faster builds
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:
- ❌
singularity-lib→space-map-lib - ❌
launch-station-lib→modularity-lib - ❌
editor-web-lib→singularity-lib
They CAN import from:
- ✅
@libs-core/*(core libraries) - ✅
@libs-foundational/core-angular(core services) - ✅ Their own components (
./components/...)
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':
- ✅ Is it a core library (
@libs-core/*)? → OK - ✅ Is it a local component (
./components/...)? → OK - ❌ Is it another platform tool? → STOP! Create shared lib or use local component
- ❌ 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:
- ✅ Keep platform tools independent
- ✅ Import from core libraries
- ✅ Use local components
- ✅ Create shared libraries for truly shared code
- ✅ Use embeddable components for cross-module UI
- ✅ Think "dependency hierarchy" not "dependency web"
DON'T:
- ❌ Import between platform tools
- ❌ Create circular references
- ❌ Assume "it will just work"
- ❌ Import from
gograb-spacein any library - ❌ Create "God libraries" that everything depends on
🚦 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