Архитектура модулей (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 | Оркестрация сценариев использования, DTO | Domain |
| Infrastructure | Внешние системы, БД, API | Domain, Application |
| Presentation | HTTP-обработка, валидация | 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-статуса
Матрица коммуникации
| Из модуля | В модуль | Паттерн | Событие/Интерфейс |
|---|---|---|---|
| Order | MLM | Событие | OrderCompletedEvent |
| Order | Notification | Событие | OrderStatusChangedEvent |
| Payment | Order | Событие | PaymentSucceededEvent |
| MLM | Users | Прямой | IUserLookupService |
| Product | MLM | Прямой | IPartnerLookupService |
| Investment | Users | Прямой | IKycStatusService |
| MLM | Notification | Событие | CommissionPaidEvent |
| MLM | Notification | Событие | RankAchievedEvent |
Правила межмодульной коммуникации
- События для побочных эффектов — Когда действие модуля A должно вызвать реакции в других модулях
- Прямая инъекция для данных — Когда модулю A нужно прочитать данные, принадлежащие модулю B
- Никогда не импортировать сущности — Через границы модулей передаются только DTO или интерфейсы
- Интерфейсы определяются в потребителе — Предотвращает циклические зависимости
- События неизменяемы — События это факты, которые произошли, их никогда не модифицируют