Перейти к основному содержимому

0002. Модульный монолит: разделение src/core и src/modules

Копия ADR из репозитория: backend/docs/adr/0002-modular-monolith-layout.md. Канонический текст — в репозитории; портал обновляется командой npm run sync:adr.

  • Статус: accepted
  • Дата: 2026-05-28
  • Связанные задачи: OP-24 (F1 · Платформа и каркас приложения)

Контекст и проблема​

ТЗ (9.1) задаёт архитектуру «модульный монолит на NestJS». Нужно зафиксировать конкретную раскладку каталогов и правила взаимодействия модулей до того, как команды начнут параллельно делать фичи (5.1–5.15) — иначе фичи будут лезть друг к другу за хранилища и конфиг разными способами, и переход к раздельным сервисам позже (если потребуется) окажется неподъёмным.

Рассмотренные варианты​

  1. Плоская структура src/<feature> — все фичи и инфраструктура на одном уровне. Просто, но смешивает горизонтальные сервисы (конфиг, БД, Redis) с доменными модулями и стирает границу «инфраструктура / бизнес-логика».
  2. src/core (инфраструктура) + src/modules (фичи) — два уровня: core содержит только горизонтальные @Global()-модули, modules — домен. Чёткая граница: что в core, то можно инжектить везде без импорта; фичи не зависят друг от друга напрямую.
  3. apps/ + libs/ (NestJS-монорепо) — встроенное разделение NestJS на приложения и библиотеки. Избыточно для одного приложения; усложняет сборку и навигацию без выгоды на текущем масштабе.

Решение​

Принят вариант 2: src/core/ + src/modules/.

  • src/core/<service>/ — только горизонтальные сервисы (конфиг, доступ к хранилищам, общие провайдеры). Объявляются @Global(); экспортируют DI-токены (DataSource из @nestjs/typeorm, REDIS_CLIENT) и фасадные сервисы (AppConfigService).
  • src/modules/<feature>/ — фичи-модули 5.1–5.15 из ТЗ. Внутри — *.module.ts, *.controller.ts, *.service.ts, dto/, entities/, *.spec.ts.
  • Доступ к окружению — только через AppConfigService. Прямое чтение process.env запрещено.
  • Доступ к PostgreSQL — через TypeORM (@InjectRepository(Entity) после TypeOrmModule.forFeature([...]), либо @InjectDataSource() для сырых запросов и транзакций; см. ADR-0001, OP-37); к Redis — через @Inject(REDIS_CLIENT) (ioredis).

Последствия​

  • Положительные: одна и та же раскладка во всех фичах; горизонтальные сервисы поднимаются один раз и реюзаются через DI; легко найти, где живёт что; пред-условие для будущей миграции отдельных модулей в самостоятельные сервисы (если/когда потребуется).
  • Отрицательные / компромиссы: дополнительный слой каталогов, чуть больше церемонии при создании новой фичи; нужен контроль ревью, чтобы инфра-код не утекал в modules/.
  • Дальше: конвенция зафиксирована в src/modules/README.md и backend/README.md. При выполнении OP-25 (health-чеки) и OP-37 (ORM) — не ломать разделение: ORM-модуль регистрируется в core/database/, health-чеки читают DataSource/REDIS_CLIENT через DI.