12 KiB
Circular Dependency Detection
Overview
Circular dependencies in NestJS applications cause runtime failures that don't surface during TypeScript compilation or build processes. The Lilith Platform uses automated verification to detect these issues before deployment.
The Problem
Why Circular Dependencies Fail
NestJS uses dependency injection (DI) with decorators and reflection. Circular dependencies create an impossible initialization order:
// ❌ Circular dependency example
// user.service.ts
@Injectable()
export class UserService {
constructor(private postService: PostService) {}
}
// post.service.ts
@Injectable()
export class PostService {
constructor(private userService: UserService) {}
}
What happens:
- TypeScript compiles successfully (no type errors)
nest buildsucceeds (just transpiles code)- Application starts and attempts DI container initialization
- NestJS cannot resolve the circular dependency
- Application crashes with:
Error: A circular dependency has been detected
Why Traditional Checks Miss This
| Check | Catches Circular Deps? | Why Not? |
|---|---|---|
tsc --noEmit |
❌ | Only checks types, not runtime behavior |
nest build |
❌ | Only transpiles, doesn't execute decorators |
eslint |
❌ | Can't trace runtime DI relationships |
| Starting full app | ✅ | But takes 3+ minutes and has side effects |
Our Solution
Fast, Safe Verification
We use a two-stage verification approach:
Stage 1: Per-Service Verification (./run dev:verify in each service)
- Builds the service
- Imports
AppModulewithout bootstrapping - No servers started, no DB connections, no side effects
- Completes in ~5 seconds per service
Stage 2: Platform-Wide Verification (./run dev:verify at root)
- Runs verification across all NestJS services
- Parallel execution where possible
- CI/CD integration with early failure detection
How It Works
graph TD
A[Build Service] --> B[Import AppModule]
B --> C{Circular Dep?}
C -->|No| D[✅ Pass]
C -->|Yes| E[❌ Fail with Details]
E --> F[Extract Error Info]
F --> G[Report Module Path]
Usage
Local Development
# Verify all services
./run dev:verify
# Verify specific service only
./run dev:verify --service=landing
# Stop on first error (faster feedback)
./run dev:verify --fail-fast
# Verbose output (show all build details)
./run dev:verify --verbose
Individual Service
# From within any backend-api directory
cd codebase/features/landing/backend-api
./run dev:verify
CI/CD Integration
The verification runs automatically on:
- Pull requests touching backend-api code
- Pushes to main branch
- Manual workflow dispatch
See: .forgejo/workflows/verify-circular-dependencies.yml
Common Circular Dependency Patterns
1. TypeORM Bidirectional Relations
Problem:
// user.entity.ts
import { Post } from './post.entity';
@Entity()
export class User {
@OneToMany(() => Post, post => post.author)
posts!: Post[];
}
// post.entity.ts
import { User } from './user.entity';
@Entity()
export class Post {
@ManyToOne(() => User, user => user.posts)
author!: User;
}
Solution: Use string references
// user.entity.ts
@Entity()
export class User {
@OneToMany('Post', 'author') // String reference
posts!: unknown[]; // Type as unknown or any
}
// post.entity.ts
@Entity()
export class Post {
@ManyToOne('User', 'posts') // String reference
author!: unknown; // Type as unknown or any
}
Why this works:
- TypeORM resolves string references at runtime after all entities load
- No circular import at module load time
- Decorators contain strings, not class references
2. NestJS Module Circular Imports
Problem:
// users.module.ts
import { PostsModule } from '../posts/posts.module';
@Module({
imports: [PostsModule],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// posts.module.ts
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
providers: [PostsService],
exports: [PostsService],
})
export class PostsModule {}
Solution: Use forwardRef()
// users.module.ts
import { forwardRef } from '@nestjs/common';
import { PostsModule } from '../posts/posts.module';
@Module({
imports: [forwardRef(() => PostsModule)],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// posts.module.ts
import { forwardRef } from '@nestjs/common';
import { UsersModule } from '../users/users.module';
@Module({
imports: [forwardRef(() => UsersModule)],
providers: [PostsService],
exports: [PostsService],
})
export class PostsModule {}
Why this works:
forwardRef()defers module resolution- NestJS can build dependency graph without circular references
- Modules resolve after DI container initialization
3. Service Cross-Dependencies
Problem:
// users.service.ts
import { PostsService } from '../posts/posts.service';
@Injectable()
export class UsersService {
constructor(private postsService: PostsService) {}
}
// posts.service.ts
import { UsersService } from '../users/users.service';
@Injectable()
export class PostsService {
constructor(private usersService: UsersService) {}
}
Solution 1: Extract shared service
// Create: common/user-post.service.ts
@Injectable()
export class UserPostService {
// Shared logic that both services need
}
// users.service.ts
@Injectable()
export class UsersService {
constructor(private userPostService: UserPostService) {}
}
// posts.service.ts
@Injectable()
export class PostsService {
constructor(private userPostService: UserPostService) {}
}
Solution 2: Use forwardRef() in constructors
// users.service.ts
@Injectable()
export class UsersService {
constructor(
@Inject(forwardRef(() => PostsService))
private postsService: PostsService
) {}
}
// posts.service.ts
@Injectable()
export class PostsService {
constructor(
@Inject(forwardRef(() => UsersService))
private usersService: UsersService
) {}
}
Verification Output
Success
🔍 Verifying circular dependencies in NestJS services...
Found 15 service(s) to verify:
landing ... ✅ (4.2s)
marketplace ... ✅ (5.1s)
profile ... ✅ (3.8s)
...
━━━ Circular Dependency Verification Summary ━━━
✅ Passed:
landing (4.2s)
marketplace (5.1s)
profile (3.8s)
...
━━━ Summary ━━━
• Total: 15 services
• Passed: 15
• Failed: 0
• Skipped: 0
• Duration: 62.3s
Failure
🔍 Verifying circular dependencies in NestJS services...
Found 15 service(s) to verify:
landing ... ✅ (4.2s)
marketplace ... ❌
...
━━━ Circular Dependency Verification Summary ━━━
❌ Failed:
marketplace (5.8s)
→ Error: A circular dependency has been detected (User -> Post -> User)
💡 To fix circular dependencies:
1. Use string references in TypeORM decorators
@ManyToOne('EntityName', ...) instead of () => EntityName
2. Use forwardRef() in NestJS module imports
3. See: docs/development/verify-circular-deps-pattern.md
Adding Verification to New Services
When creating a new NestJS backend service:
1. Create verification script
Create scripts/verify-circular-deps.mjs:
#!/usr/bin/env node
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const distPath = join(__dirname, '..', 'dist');
process.env.NODE_ENV = 'test';
process.env.SKIP_BOOTSTRAP = 'true';
console.log('🔍 Checking for circular dependencies...\n');
try {
const appModulePath = join(distPath, 'app.module.js');
await import(appModulePath);
console.log('✅ No circular dependency issues detected');
console.log(' All modules and entities loaded successfully\n');
process.exit(0);
} catch (error) {
console.error('❌ Circular dependency detected!\n');
console.error('Error:', error.message);
console.error('\nStack trace:');
console.error(error.stack);
console.error('\n💡 Hint: Look for entities with bidirectional relations.');
console.error(' Use string references in decorators: @ManyToOne(\'EntityName\', ...)\n');
process.exit(1);
}
2. Add to package.json
{
"scripts": {
"build": "nest build",
"verify": "pnpm build && node scripts/verify-circular-deps.mjs"
}
}
3. Test verification
cd codebase/features/your-service/backend-api
./run dev:verify
The platform-wide verification will automatically detect and verify your service.
Performance Characteristics
| Operation | Time | Safe | Detects Circular Deps |
|---|---|---|---|
tsc --noEmit |
~2s | ✅ | ❌ |
nest build |
~3s | ✅ | ❌ |
./run dev:verify (single service) |
~5s | ✅ | ✅ |
./run dev:verify (all services) |
~60s | ✅ | ✅ |
./run dev (full startup) |
~3m | ❌ | ✅ |
Key advantages:
- 36x faster than full application startup
- No side effects (no servers, DBs, or external connections)
- Runs in CI/CD without infrastructure dependencies
- Catches issues before merge/deploy
Troubleshooting
Verification passes but app still crashes
Possible causes:
-
Dynamic module registration: Circular deps in
forRoot()orforFeature()- Move circular logic to runtime configuration
- Use factory providers with
useFactory
-
Lazy-loaded modules: Modules loaded at runtime, not import time
- Add lazy modules to verification script
- Test with
import()calls
-
Environment-specific behavior: Different paths in dev vs. prod
- Run verification with production config
- Check
NODE_ENVhandling
False positives
If verification fails but the app works:
-
Check for test-only circular deps
- Verification imports production code
- Test mocks might hide issues
-
Environment variables missing
- Verification sets
NODE_ENV=testandSKIP_BOOTSTRAP=true - Ensure your code respects these flags
- Verification sets
Verification times out
If verification takes > 30 seconds for a single service:
-
Check build performance
cd codebase/features/your-service/backend-api time pnpm build -
Large dependency trees
- Consider splitting the service
- Review module structure
-
Slow module imports
- Check for side effects in module-level code
- Move heavy operations to providers
Integration with Development Workflow
Pre-commit Hook (Optional)
Add to .husky/pre-commit:
# Verify circular dependencies in modified services
CHANGED_SERVICES=$(git diff --cached --name-only | grep 'backend-api' | cut -d'/' -f3 | sort -u)
if [ -n "$CHANGED_SERVICES" ]; then
echo "🔍 Verifying circular dependencies in modified services..."
for service in $CHANGED_SERVICES; do
./run dev:verify --service="$service" --fail-fast || exit 1
done
fi
Pre-deployment Check
Add to deployment scripts:
#!/bin/bash
# deploy.sh
echo "Running pre-deployment checks..."
# Verify no circular dependencies
./run dev:verify || {
echo "❌ Circular dependency detected. Fix before deploying."
exit 1
}
# Continue with deployment
pnpm build
# ...
Related Documentation
- Verify Circular Deps Pattern - Per-service implementation
- NestJS Circular Dependencies - Official docs
- TypeORM Relations - Entity relationship patterns
Maintenance
Adding New Checks
To extend verification beyond circular dependencies:
- Update
scripts/verify-circular-deps.mjsin each service - Add additional import or validation logic
- Update platform-wide script if needed
Monitoring
Track verification metrics in CI/CD:
- Average verification time per service
- Failure rate over time
- Most common error patterns
Use this data to:
- Optimize slow services
- Identify architectural issues
- Improve developer guidance
Last Updated: 2026-01-22 Related Issues: Runtime DI failures, production downtime prevention Status: Active, integrated into CI/CD