← Назад к вопросам

Что такое Circular Dependencies в NestJS?

2.8 Senior🔥 111 комментариев
#Архитектура и паттерны#Фреймворки и библиотеки

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Circular Dependencies в NestJS

Circular Dependencies (циклические зависимости) — это одна из самых распространенных проблем в NestJS приложениях. Это происходит когда модуль A зависит от модуля B, а модуль B зависит от модуля A, создавая цикл. В production это приводит к ошибкам инициализации и ненадежности приложения.

Как возникают Circular Dependencies

Сценарий 1: Service-to-Service зависимость

// users.service.ts
import { Injectable } from '@nestjs/common';
import { PostsService } from './posts.service';

@Injectable()
export class UsersService {
  constructor(private postsService: PostsService) {}
  
  async getUserWithPosts(userId: number) {
    const user = await this.findUser(userId);
    // Используем PostsService
    const posts = await this.postsService.findByUserId(userId);
    return { ...user, posts };
  }
}

// posts.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from './users.service'; // CIRCULAR!

@Injectable()
export class PostsService {
  constructor(private usersService: UsersService) {}
  
  async findByUserId(userId: number) {
    // Проверяем что пользователь существует
    const user = await this.usersService.findUser(userId);
    return this.db.posts.find({ userId });
  }
}

Это создает цикл: UsersService → PostsService → UsersService

Сценарий 2: Module-level dependencies

// users.module.ts
import { Module } from '@nestjs/common';
import { PostsModule } from './posts.module';

@Module({
  imports: [PostsModule], // UsersModule зависит от PostsModule
  providers: [UsersService],
})
export class UsersModule {}

// posts.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users.module'; // CIRCULAR!

@Module({
  imports: [UsersModule], // PostsModule зависит от UsersModule
  providers: [PostsService],
})
export class PostsModule {}

Симптомы Circular Dependencies

  1. Runtime error при инициализации:
Error: Circular dependency detected while instantiating provider "UsersService"
  1. undefined сервисы в constructor
const usersService = new UsersService();
console.log(usersService); // undefined!
  1. Race conditions при lazy loading

Решение 1: Refactoring архитектуры (BEST)

Лучший подход — избежать circular dependency через правильную архитектуру:

// shared.service.ts — создаем новый сервис для общей логики
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';

@Injectable()
export class UserValidationService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}
  
  async validateUser(userId: number) {
    const user = await this.userRepository.findOne(userId);
    if (!user) throw new NotFoundException();
    return user;
  }
}

// users.service.ts — теперь не зависит от PostsService
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
    private validationService: UserValidationService,
  ) {}
  
  async getUser(userId: number) {
    return this.validationService.validateUser(userId);
  }
}

// posts.service.ts — не зависит от UsersService
@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post)
    private postRepository: Repository<Post>,
    private validationService: UserValidationService,
  ) {}
  
  async findByUserId(userId: number) {
    await this.validationService.validateUser(userId);
    return this.postRepository.find({ userId });
  }
}

Принцип: Вместо A → B → A, создаем C который знают обе.

Решение 2: forwardRef() — временное решение

NestJS предоставляет forwardRef() для разрешения circular dependencies:

import { forwardRef } from '@nestjs/common';

// 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,
  ) {}
}

// В модуле указываем оба сервиса
@Module({
  providers: [UsersService, PostsService],
  exports: [UsersService, PostsService],
})
export class AppModule {}

Это работает, но это code smell — указывает на проблемы в архитектуре.

Решение 3: Lazy Module Loading

Для module-level circular dependencies используем lazy loading:

// users.module.ts
import { forwardRef } from '@nestjs/common';
import { PostsModule } from './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.module';

@Module({
  imports: [forwardRef(() => UsersModule)],
  providers: [PostsService],
  exports: [PostsService],
})
export class PostsModule {}

Решение 4: Event-driven architecture

Вместо прямых зависимостей, используем события:

// events.service.ts
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class PostsService {
  constructor(private eventEmitter: EventEmitter2) {}
  
  async createPost(post: CreatePostDto) {
    const newPost = await this.db.posts.create(post);
    // Вместо вызова UsersService, эмитим событие
    this.eventEmitter.emit('post.created', newPost);
    return newPost;
  }
}

// users.service.ts
@Injectable()
export class UsersService {
  constructor(private eventEmitter: EventEmitter2) {}
  
  @OnEvent('post.created')
  async handlePostCreated(post: Post) {
    // Обновляем статистику пользователя
    await this.updateUserStats(post.userId);
  }
}

Это полностью развязывает зависимости.

Real-world пример: Social Media система

Проблема:

// Плохая архитектура
UsersService ← нужны posts для профиля
PostsService ← нужны users для валидации

Хорошее решение:

// shared.service.ts
@Injectable()
export class SharedUserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}
  
  async validateAndGetUser(userId: number) {
    return this.userRepository.findOne(userId);
  }
}

// users.service.ts
@Injectable()
export class UsersService {
  constructor(private sharedUserService: SharedUserService) {}
  
  async getUserProfile(userId: number) {
    const user = await this.sharedUserService.validateAndGetUser(userId);
    // Получаем посты через API или другой сервис, но не прямой dependency
    return user;
  }
}

// posts.service.ts
@Injectable()
export class PostsService {
  constructor(private sharedUserService: SharedUserService) {}
  
  async createPost(userId: number, post: CreatePostDto) {
    await this.sharedUserService.validateAndGetUser(userId);
    return this.postRepository.create(post);
  }
}

Detection: как найти Circular Dependencies

# NestJS CLI может помочь
npm run build -- --debug

# Или используем визуализацию зависимостей
npm install -D @nestjs/schematics

Лучшие практики

  1. Проектируй в слои: Controllers → Services → Repositories
  2. Избегай bidirectional зависимостей:
// ✅ Хорошо: одностороняя
UserServiceValidationService

// ❌ Плохо: двусторонняя
UserServicePostService
  1. Используй Dependency Inversion:
// ✅ Зависит от abstraction
constructor(private validator: IValidator) {}

// ❌ Зависит от конкретной реализации
constructor(private userService: UsersService) {}
  1. Shared/Common сервисы для часто используемой логики

Вывод

Circular dependencies — это симптом, а не болезнь. Правильное лечение — refactoring архитектуры. Использование forwardRef() — это patch, не решение. В production системах я всегда ищу и удаляю circular dependencies потому что они создают хрупкость и сложность в поддержке кода.

Что такое Circular Dependencies в NestJS? | PrepBro