Circular Dependency trong NestJS
Circular Dependency (phụ thuộc vòng tròn) xảy ra khi Module A imports Module B và Module B imports Module A, tạo thành một vòng lặp vô tận. Đây là một vấn đề phổ biến khi xây dựng các ứng dụng phức tạp, và NestJS cung cấp nhiều cách để giải quyết nó.
Khái Niệm Circular Dependency
Circular dependency là khi:
- Module A → Module B → Module A
- Service A → Service B → Service A
- Class A → Class B → Class A
// ❌ Lỗi - Circular dependency
// users.module.ts
@Module({
imports: [PostsModule], // imports PostsModule
})
export class UsersModule {}
// posts.module.ts
@Module({
imports: [UsersModule], // imports UsersModule - CIRCULAR!
})
export class PostsModule {}
Dấu Hiệu Circular Dependency
Error: Cannot find module
Error: Circular module dependency detected
Maximum call stack size exceeded
ReferenceError: [ClassName] is not defined
Ví Dụ Circular Dependency
1. Service-Level Circular Dependency
// users.service.ts
@Injectable()
export class UsersService {
constructor(private postsService: PostsService) {} // Dependency on PostsService
getUser(id: string) {
return { id, name: 'John' };
}
getUserWithPosts(id: string) {
const user = this.getUser(id);
const posts = this.postsService.getPostsByUser(id); // Using PostsService
return { user, posts };
}
}
// posts.service.ts
@Injectable()
export class PostsService {
constructor(private usersService: UsersService) {} // Dependency on UsersService - CIRCULAR!
getPost(id: string) {
return { id, title: 'Post Title' };
}
getPostsByUser(userId: string) {
const user = this.usersService.getUser(userId); // Using UsersService
return [{ id: 1, userId, title: 'Post Title' }];
}
}
2. Module-Level Circular Dependency
// users.module.ts
@Module({
imports: [PostsModule], // UsersModule depends on PostsModule
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
// posts.module.ts
@Module({
imports: [UsersModule], // PostsModule depends on UsersModule - CIRCULAR!
providers: [PostsService],
controllers: [PostsController],
})
export class PostsModule {}
Cách Giải Quyết Circular Dependency
1. Forward Reference (forwardRef)
Sử dụng forwardRef() để delay resolution của dependency:
import { forwardRef } from '@nestjs/common';
// users.service.ts
@Injectable()
export class UsersService {
constructor(
@Inject(forwardRef(() => PostsService))
private postsService: PostsService,
) {}
getUser(id: string) {
return { id, name: 'John' };
}
getUserWithPosts(id: string) {
const user = this.getUser(id);
const posts = this.postsService.getPostsByUser(id);
return { user, posts };
}
}
// posts.service.ts
@Injectable()
export class PostsService {
constructor(
@Inject(forwardRef(() => UsersService))
private usersService: UsersService,
) {}
getPost(id: string) {
return { id, title: 'Post Title' };
}
getPostsByUser(userId: string) {
const user = this.usersService.getUser(userId);
return [{ id: 1, userId, title: 'Post Title' }];
}
}
// users.module.ts
@Module({
imports: [PostsModule],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
// posts.module.ts
@Module({
imports: [UsersModule],
providers: [PostsService],
controllers: [PostsController],
})
export class PostsModule {}
2. Shared Module (Recommended)
Tạo shared module để break circular dependency:
// shared.module.ts
@Module({
providers: [SharedService],
exports: [SharedService],
})
export class SharedModule {}
@Injectable()
export class SharedService {
getSharedData() {
return { shared: true };
}
}
// users.module.ts
@Module({
imports: [SharedModule],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
// posts.module.ts
@Module({
imports: [SharedModule],
providers: [PostsService],
controllers: [PostsController],
})
export class PostsModule {}
// users.service.ts
@Injectable()
export class UsersService {
constructor(private sharedService: SharedService) {}
getUser(id: string) {
const shared = this.sharedService.getSharedData();
return { id, name: 'John', shared };
}
}
// posts.service.ts
@Injectable()
export class PostsService {
constructor(private sharedService: SharedService) {}
getPost(id: string) {
const shared = this.sharedService.getSharedData();
return { id, title: 'Post Title', shared };
}
}
3. ModuleRef (Dynamic Resolution)
Sử dụng ModuleRef để dynamically resolve dependencies:
import { ModuleRef } from '@nestjs/core';
// users.service.ts
@Injectable()
export class UsersService {
constructor(private moduleRef: ModuleRef) {}
getUser(id: string) {
return { id, name: 'John' };
}
getUserWithPosts(id: string) {
const user = this.getUser(id);
// Dynamically get PostsService
const postsService = this.moduleRef.get(PostsService, { strict: false });
if (postsService) {
const posts = postsService.getPostsByUser(id);
return { user, posts };
}
return { user };
}
}
// posts.service.ts
@Injectable()
export class PostsService {
constructor(private moduleRef: ModuleRef) {}
getPost(id: string) {
return { id, title: 'Post Title' };
}
getPostsByUser(userId: string) {
// Dynamically get UsersService
const usersService = this.moduleRef.get(UsersService, { strict: false });
if (usersService) {
const user = usersService.getUser(userId);
return [{ id: 1, userId, title: 'Post Title', user }];
}
return [{ id: 1, userId, title: 'Post Title' }];
}
}
// users.module.ts
@Module({
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
// posts.module.ts
@Module({
providers: [PostsService],
controllers: [PostsController],
})
export class PostsModule {}
4. Lazy Property Injection
// users.service.ts
@Injectable()
export class UsersService {
private postsService: PostsService;
constructor(private moduleRef: ModuleRef) {}
// Lazy load postsService khi cần
private getPostsService() {
if (!this.postsService) {
this.postsService = this.moduleRef.get(PostsService);
}
return this.postsService;
}
getUser(id: string) {
return { id, name: 'John' };
}
getUserWithPosts(id: string) {
const user = this.getUser(id);
const postsService = this.getPostsService();
const posts = postsService.getPostsByUser(id);
return { user, posts };
}
}
// posts.service.ts
@Injectable()
export class PostsService {
private usersService: UsersService;
constructor(private moduleRef: ModuleRef) {}
private getUsersService() {
if (!this.usersService) {
this.usersService = this.moduleRef.get(UsersService);
}
return this.usersService;
}
getPost(id: string) {
return { id, title: 'Post Title' };
}
getPostsByUser(userId: string) {
const usersService = this.getUsersService();
const user = usersService.getUser(userId);
return [{ id: 1, userId, title: 'Post Title', user }];
}
}
Ví Dụ Thực Tế
1. User-Post Relationship
// entities/user.entity.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(() => Post, (post) => post.user, { lazy: true })
posts: Post[];
}
// entities/post.entity.ts
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToOne(() => User, (user) => user.posts)
user: User;
}
// Problem: TypeORM circular reference
// Solution: Use lazy loading hoặc forwardRef
// users/users.service.ts
@Injectable()
export class UsersService {
constructor(
private readonly usersRepository: Repository<User>,
@Inject(forwardRef(() => PostsService))
private postsService: PostsService,
) {}
async getUserWithPosts(id: number) {
const user = await this.usersRepository.findOne({
where: { id },
relations: ['posts'],
});
return user;
}
}
// posts/posts.service.ts
@Injectable()
export class PostsService {
constructor(
private readonly postsRepository: Repository<Post>,
@Inject(forwardRef(() => UsersService))
private usersService: UsersService,
) {}
async getPostWithUser(id: number) {
const post = await this.postsRepository.findOne({
where: { id },
relations: ['user'],
});
return post;
}
}
// users.module.ts
@Module({
imports: [PostsModule],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
// posts.module.ts
@Module({
imports: [UsersModule],
providers: [PostsService],
controllers: [PostsController],
})
export class PostsModule {}
2. Authentication-Authorization Circular Dependency
// auth.service.ts
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async login(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (!user || !(await this.validatePassword(password, user.password))) {
throw new UnauthorizedException('Invalid credentials');
}
const token = this.jwtService.sign({ sub: user.id });
return { access_token: token };
}
}
// users.service.ts
@Injectable()
export class UsersService {
constructor(
@Inject(forwardRef(() => AuthService))
private authService: AuthService,
) {}
async create(createUserDto: CreateUserDto) {
const user = await this.usersRepository.save(createUserDto);
// Send welcome email with login instructions
await this.sendWelcomeEmail(user.email);
return user;
}
async findByEmail(email: string) {
return this.usersRepository.findOne({ where: { email } });
}
}
// auth.module.ts
@Module({
imports: [UsersModule, JwtModule],
providers: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
// users.module.ts
@Module({
imports: [forwardRef(() => AuthModule)],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
3. Notification Service Circular Dependency
// notification.service.ts
@Injectable()
export class NotificationService {
constructor(private moduleRef: ModuleRef) {}
async notifyUser(userId: number, message: string) {
// Get user service dynamically
const usersService = this.moduleRef.get(UsersService, { strict: false });
if (usersService) {
const user = await usersService.findOne(userId);
console.log(`Notifying ${user.email}: ${message}`);
}
}
}
// users.service.ts
@Injectable()
export class UsersService {
constructor(private moduleRef: ModuleRef) {}
async createUser(createUserDto: CreateUserDto) {
const user = await this.usersRepository.save(createUserDto);
// Get notification service dynamically
const notificationService = this.moduleRef.get(NotificationService, {
strict: false,
});
if (notificationService) {
await notificationService.notifyUser(user.id, 'Welcome!');
}
return user;
}
}
// app.module.ts
@Module({
imports: [UsersModule, NotificationsModule],
})
export class AppModule {}
4. Order-Payment Circular Dependency
// orders.service.ts
@Injectable()
export class OrdersService {
constructor(
private ordersRepository: Repository<Order>,
@Inject(forwardRef(() => PaymentService))
private paymentService: PaymentService,
) {}
async createOrder(createOrderDto: CreateOrderDto) {
const order = await this.ordersRepository.save(createOrderDto);
// Process payment
const payment = await this.paymentService.processPayment(
order.id,
order.amount,
);
return { order, payment };
}
async getOrder(id: number) {
return this.ordersRepository.findOne({ where: { id } });
}
}
// payments.service.ts
@Injectable()
export class PaymentService {
constructor(
private paymentsRepository: Repository<Payment>,
@Inject(forwardRef(() => OrdersService))
private ordersService: OrdersService,
) {}
async processPayment(orderId: number, amount: number) {
const order = await this.ordersService.getOrder(orderId);
if (!order) {
throw new NotFoundException('Order not found');
}
const payment = await this.paymentsRepository.save({
orderId,
amount,
status: 'completed',
});
return payment;
}
}
// orders.module.ts
@Module({
imports: [PaymentsModule],
providers: [OrdersService],
controllers: [OrdersController],
})
export class OrdersModule {}
// payments.module.ts
@Module({
imports: [OrdersModule],
providers: [PaymentService],
controllers: [PaymentController],
})
export class PaymentsModule {}
Detecting Circular Dependencies
1. Visual Inspection
// Draw dependency graph
Users → Posts → Comments → Users (CIRCULAR!)
// Better structure
Users → Posts → Comments
↓
Shared Services
2. Runtime Detection
@Injectable()
export class CircularDependencyDetector {
constructor(private moduleRef: ModuleRef) {}
detectCycles(module: any, visited = new Set()): boolean {
if (visited.has(module)) {
console.warn(`Circular dependency detected: ${module.name}`);
return true;
}
visited.add(module);
// Check module's imports
const metadata = Reflect.getMetadata('imports', module);
if (metadata) {
for (const importedModule of metadata) {
if (this.detectCycles(importedModule, new Set(visited))) {
return true;
}
}
}
return false;
}
}
Best Practices
1. Prefer Shared Module Pattern
// ❌ Sai - Circular dependency with forwardRef
@Injectable()
export class ServiceA {
constructor(@Inject(forwardRef(() => ServiceB)) private serviceB: ServiceB) {}
}
@Injectable()
export class ServiceB {
constructor(@Inject(forwardRef(() => ServiceA)) private serviceA: ServiceA) {}
}
// ✅ Đúng - Use shared module
@Module({
providers: [SharedService],
exports: [SharedService],
})
export class SharedModule {}
@Injectable()
export class ServiceA {
constructor(private sharedService: SharedService) {}
}
@Injectable()
export class ServiceB {
constructor(private sharedService: SharedService) {}
}
2. Use forwardRef Only When Necessary
// ❌ Overuse forwardRef
@Inject(forwardRef(() => Service)) service: Service
// ✅ Use ModuleRef instead
constructor(private moduleRef: ModuleRef) {
const service = this.moduleRef.get(Service);
}
3. Architecture Planning
// ✅ Đúng - Think about architecture
// 1. Core domain layer
// 2. Application layer
// 3. Infrastructure layer
// 4. Presentation layer
// Minimize cross-layer dependencies
4. Use Interfaces/Abstraction
// ✅ Đúng - Program to interfaces
export interface IUserService {
findOne(id: number): Promise<User>;
}
@Injectable()
export class UserService implements IUserService {
async findOne(id: number) {
return this.usersRepository.findOne({ where: { id } });
}
}
5. Lazy Loading Dependencies
// ✅ Đúng - Lazy load when needed
@Injectable()
export class ServiceA {
private serviceBInstance: ServiceB;
constructor(private moduleRef: ModuleRef) {}
getServiceB(): ServiceB {
if (!this.serviceBInstance) {
this.serviceBInstance = this.moduleRef.get(ServiceB);
}
return this.serviceBInstance;
}
}
Complete Example: Breaking Circular Dependency
// ❌ BEFORE - Circular Dependency
// users.service.ts
@Injectable()
export class UsersService {
constructor(private postsService: PostsService) {}
getUserWithPosts(id: number) {
const user = { id, name: 'John' };
const posts = this.postsService.getPostsByUser(id);
return { user, posts };
}
}
// posts.service.ts
@Injectable()
export class PostsService {
constructor(private usersService: UsersService) {}
getPostsByUser(userId: number) {
const user = this.usersService.getUser(userId);
return [{ id: 1, userId, title: 'Post Title' }];
}
}
// ✅ AFTER - Fixed with Shared Module
// shared.module.ts
@Module({
providers: [SharedDataService],
exports: [SharedDataService],
})
export class SharedModule {}
@Injectable()
export class SharedDataService {
getUser(id: number) {
return { id, name: 'John' };
}
getPostsByUser(userId: number) {
return [{ id: 1, userId, title: 'Post Title' }];
}
}
// users.service.ts
@Injectable()
export class UsersService {
constructor(private sharedService: SharedDataService) {}
getUserWithPosts(id: number) {
const user = this.sharedService.getUser(id);
const posts = this.sharedService.getPostsByUser(id);
return { user, posts };
}
}
// posts.service.ts
@Injectable()
export class PostsService {
constructor(private sharedService: SharedDataService) {}
getPostsByUser(userId: number) {
const user = this.sharedService.getUser(userId);
return [{ id: 1, userId, title: 'Post Title', user }];
}
}
// users.module.ts
@Module({
imports: [SharedModule],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
// posts.module.ts
@Module({
imports: [SharedModule],
providers: [PostsService],
controllers: [PostsController],
})
export class PostsModule {}
Troubleshooting
// Error: Cannot find module [module name]
// Solution: Check for circular imports
// Error: Circular module dependency detected
// Solution: Use forwardRef or break into shared module
// Error: Maximum call stack size exceeded
// Solution: Likely infinite loop - refactor architecture
// Error: Provider not found
// Solution: Ensure module exports the provider
Kết Luận
Circular Dependency là vấn đề phổ biến nhưng có thể tránh được bằng:
- Shared Module Pattern (Recommended)
- ForwardRef (Quick fix)
- ModuleRef (Flexible solution)
- Lazy Property Injection (For complex scenarios)
- Architecture Planning (Best prevention)
Best practice là:
- Thiết kế architecture cẩn thận từ đầu
- Sử dụng shared modules để break cycles
- Tránh forwardRef nếu có thể
- Refactor early khi detect circular dependencies
- Use dependency graphs để visualize relationships
Điều quan trọng là hiểu rõ why circular dependencies xảy ra, sau đó fix it properly!