Derived Modular Arch
Концепции

Модули и public API

Стадии роста модуля, папка public/, сегменты и правила импорта между модулями.

Модуль в DMA — это папка (или один файл) внутри features/ или services/. Правила одинаковые для обоих слоёв: отличается только предикат слоя, а не структура внутри.

Главная идея: модуль растёт постепенно, без миграций «переписать всё с нуля». Каждая стадия — надстройка над предыдущей.

Стадии роста

Стадия 0 — файл-модуль

Пока в модуле один файл — весь файл и есть публичная поверхность:

features/
└── checkout.tsx

Импортируйте файл напрямую, без папки и без index.ts.

Стадия 1 — плоская папка

Появились соседние файлы с тем же basename-префиксом, что у file-модуля (например checkout.tsx и checkout.store.ts в features/) — заводите папку и public/:

features/checkout/
├── public/
│   └── checkout-page.tsx
├── use-cart-total.ts
└── cart-row.tsx

Всё, что нужно снаружи, лежит в public/. Внутренние файлы — рядом, но импортировать их из других модулей нельзя.

Стадия 2 — сегменты

Когда внутри накопилось много файлов (ориентир — около 8+, сигнал stage-growth в dma doctor), делите по ролям:

features/checkout/
├── public/     # точки входа — плоская папка
├── ui/
├── model/
├── api/
└── lib/        # хелперы, которые нужны 2+ сегментам этого модуля
СегментЧто внутри
public/Entrypoints, события, порты, внешние типы
ui/Компоненты презентации
model/Состояние и доменная логика модуля
api/Транспорт, маппинг DTO
lib/Чистые хелперы для нескольких сегментов

Рекомендуемое направление внутри модуля (включайте, когда начинает мешать, не с первого дня):

public → ui, model, api, lib
ui     → model, lib
model  → api, lib
api    → lib

Имена совпадают с группами в shared/, но права другие: features/checkout/ui может импортировать model того же модуля, а shared/ui — не может тянуть shared/model.

Стадия 3 — разделение

Если в public/ слишком много реализаций точка входа'ов (ориентир ~8+) — делите модуль, а не вкладывайте public/ друг в друга. Пока это решение на ревью, отдельного сигнала в doctor нет.

Стадия 4 — пакет

Несколько приложений потребляют модуль → выносите в пакет монорепо. Правила те же, enforcement через exports workspace-пакета.

Публичный API

Межмодульный импорт — только в */public/*, напрямую в файл:

// ✓
import { CheckoutPage } from "@/features/checkout/public/checkout-page";

// ✗ глубокий импорт во внутренности
import { CartRow } from "@/features/checkout/ui/cart-row";

// ✗ barrel
import { CheckoutPage } from "@/features/checkout";

public/ остаётся плоским. Реализация точка входа'а живёт в public/ и может импортировать внутренние сегменты. Реэкспорт 1:1 из внутреннего файла допустим, но по умолчанию пишите код прямо в public/.

На стадии 0 весь файл — публичный, папка public/ не нужна.

Barrel-файлы запрещены

index.ts с реэкспортами внутри модуля — нельзя. Такой файл прячет граф зависимостей. Подробнее — Почему нет barrel-файлов.

Колокация внутри модуля

  • Тесты *.test.ts — рядом с тестируемым файлом
  • Стили — рядом с компонентом
  • Типы — рядом с потребителем; межмодульные типы — public/*.types.ts или shared/domain после второго использования
  • Имена файлов — kebab-case, смысл в имени: use-cart-total.ts, не hook.ts

Связь между модулями

ЗадачаКак
Использовать код ниже по слоямПрямой импорт services/*/public/* или shared/
Событие без знания подписчиковСобытие в public/ эмиттера; подписка вниз или связка в app/
Прямой импорт дал бы цикл или шаг вверхПорт в public/ports.ts + привязка в корень композиции
Только визуальная композицияСлоты/props в корень композиции

Порты — зависимость во время выполнения, невидимая для статического графа. Держите их в public/ports.ts и не используйте для обхода promotion.

Что проверяют инструменты

ПравилоЧто ловит
public-apiИмпорт мимо public/
no-barrelBarrel index с реэкспортами
stage-growth (doctor)Структура отстаёт от размера модуля

Что дальше

On this page