Skip to content

Архитектура модулей (Clean Architecture)

Каждый модуль следует данной структуре:

module/
├── domain/                # Бизнес-логика (без зависимостей от фреймворка)
│   ├── entities/         # Доменные сущности
│   ├── value-objects/    # Неизменяемые объекты-значения
│   ├── services/         # Доменные сервисы
│   ├── events/           # Доменные события
│   └── interfaces/       # Определения портов (репозитории, сервисы)

├── application/          # Сценарии использования
│   ├── commands/         # Операции записи (CQRS)
│   ├── queries/          # Операции чтения
│   ├── dtos/             # Объекты передачи данных
│   └── handlers/         # Обработчики событий

├── infrastructure/       # Адаптеры
│   ├── repositories/     # Реализации для базы данных
│   ├── services/         # Реализации внешних сервисов
│   └── mappers/          # Маппинг Entity <-> DTO

└── presentation/         # Слой API
    ├── controllers/      # HTTP-контроллеры
    ├── guards/           # Гарды авторизации
    └── validators/       # Валидаторы запросов

Ответственности слоёв

СлойОтветственностьЗависимости
DomainБизнес-правила, сущности, интерфейсыНет (чистый)
ApplicationОркестрация сценариев использования, DTODomain
InfrastructureВнешние системы, БД, APIDomain, Application
PresentationHTTP-обработка, валидацияApplication

Правило зависимостей

Зависимости всегда направлены внутрь:

  • Presentation → Application → Domain
  • Infrastructure → Domain (реализует интерфейсы)

Недопустимо:

  • Domain → Infrastructure
  • Domain → Presentation

Межмодульная коммуникация

В рамках модульного монолита модулям необходимо взаимодействовать. В зависимости от требований связности используются два паттерна:

Паттерн 1: Доменные события (предпочтительный для кросс-модульного взаимодействия)

Используется когда: Действие модуля A должно вызвать реакцию модуля B, но A не должен знать о B.

┌─────────────┐     публикует      ┌──────────────┐     обрабатывает  ┌─────────────┐
│   Модуль    │ ──────────────────→│  EventBus    │──────────────────→│   Модуль    │
│   Order     │   OrderCompleted   │  (in-memory) │   OrderCompleted  │    MLM      │
└─────────────┘                    └──────────────┘                   └─────────────┘

Реализация:

typescript
// Определение доменного события (в модуле order)
export class OrderCompletedEvent {
  constructor(
    public readonly orderId: string,
    public readonly userId: string,
    public readonly totalAmount: number,
    public readonly referringPartnerId: string | null,
  ) {}
}

// Публикация (в слое application модуля order)
@Injectable()
export class CompleteOrderCommand {
  constructor(private readonly eventBus: EventBus) {}

  async execute(orderId: string): Promise<void> {
    // ... логика завершения заказа ...

    // Публикация события - модуль order не знает, кто слушает
    this.eventBus.publish(new OrderCompletedEvent(
      order.id,
      order.userId,
      order.total,
      order.referringPartnerId,
    ));
  }
}

// Обработка (в слое application модуля mlm)
@Injectable()
export class OrderCompletedHandler {
  constructor(private readonly commissionService: CommissionCalculationService) {}

  @OnEvent(OrderCompletedEvent)
  async handle(event: OrderCompletedEvent): Promise<void> {
    if (event.referringPartnerId) {
      await this.commissionService.calculateForOrder(
        event.orderId,
        event.totalAmount,
        event.referringPartnerId,
      );
    }
  }
}

Когда использовать:

  • Завершение заказа → Расчёт комиссии
  • Успешная оплата → Подтверждение заказа
  • Регистрация партнёра → Приветственное уведомление
  • Достижение ранга → Триггер бонусной выплаты

Паттерн 2: Прямая инъекция сервисов (для тесной интеграции)

Используется когда: Модулю A синхронно нужны данные от модуля B, и связность допустима.

typescript
// Интерфейс определён в ПОТРЕБЛЯЮЩЕМ модуле (mlm)
// чтобы избежать циклических зависимостей
export interface IUserLookupService {
  getUserById(userId: string): Promise<UserBasicInfo | null>;
  getUserKycStatus(userId: string): Promise<KycStatus>;
}

// Реализация в ПРЕДОСТАВЛЯЮЩЕМ модуле (users)
@Injectable()
export class UserLookupService implements IUserLookupService {
  constructor(private readonly userRepository: UserRepository) {}

  async getUserById(userId: string): Promise<UserBasicInfo | null> {
    const user = await this.userRepository.findById(userId);
    return user ? { id: user.id, email: user.email, name: user.name } : null;
  }
}

// Регистрация в модуле users
@Module({
  providers: [
    {
      provide: 'IUserLookupService',
      useClass: UserLookupService,
    },
  ],
  exports: ['IUserLookupService'],
})
export class UsersModule {}

// Использование в модуле mlm
@Injectable()
export class PartnerService {
  constructor(
    @Inject('IUserLookupService')
    private readonly userLookup: IUserLookupService,
  ) {}

  async getPartnerWithUser(partnerId: string) {
    const partner = await this.partnerRepository.findById(partnerId);
    const user = await this.userLookup.getUserById(partner.userId);
    return { ...partner, user };
  }
}

Когда использовать:

  • Модулю MLM нужны данные профиля пользователя
  • Модулю Product нужна информация о реферальном партнёре
  • Модулю Investment нужна проверка KYC-статуса

Матрица коммуникации

Из модуляВ модульПаттернСобытие/Интерфейс
OrderMLMСобытиеOrderCompletedEvent
OrderNotificationСобытиеOrderStatusChangedEvent
PaymentOrderСобытиеPaymentSucceededEvent
MLMUsersПрямойIUserLookupService
ProductMLMПрямойIPartnerLookupService
InvestmentUsersПрямойIKycStatusService
MLMNotificationСобытиеCommissionPaidEvent
MLMNotificationСобытиеRankAchievedEvent

Правила межмодульной коммуникации

  1. События для побочных эффектов — Когда действие модуля A должно вызвать реакции в других модулях
  2. Прямая инъекция для данных — Когда модулю A нужно прочитать данные, принадлежащие модулю B
  3. Никогда не импортировать сущности — Через границы модулей передаются только DTO или интерфейсы
  4. Интерфейсы определяются в потребителе — Предотвращает циклические зависимости
  5. События неизменяемы — События это факты, которые произошли, их никогда не модифицируют

Связанные документы