Четыре инварианта
Правила, которые DMA держит на каждом масштабе — от pet-проекта до монорепо.
Инвариант в DMA — это правило, которое не зависит от размера команды и проекта. Четыре таких правила покрывают направление зависимостей, границы модулей и то, как код растёт со временем.
Если правило нельзя проверить инструментом — это пожелание, а не часть архитектуры. Исключение одно: граница между services и shared частично остаётся на ваше усмотрение (см. Слои).
1. Импорты только вниз
Модули импортируют только из слоёв ниже по рангу:
app/pages/routes → features → services → sharedКорень композиции (app/, pages/, routes/) собирает приложение. Features не знают друг о друге. Services могут зависеть от других services, но граф остаётся ацикличным. Shared — только внутри shared.
Зачем: циклы и «утечки вверх» — главная причина, почему рефакторинг становится дорогим. Направление фиксировано, его видно в графе.
Что проверяют инструменты: правила layer-direction, feature-to-feature, no-cycle.
2. Публичный API без barrel-файлов
Когда один модуль использует другой, импорт идёт напрямую в файл внутри */public/*:
// ✓
import { CartPanel } from "@/features/cart/public/cart-panel";
// ✗ barrel прячет, откуда реально тянется зависимость
import { CartPanel } from "@/features/cart/public";У маленьких модулей (стадия 0) весь файл может быть публичным — без папки public/. Как только модуль обрастает внутренностями, публичная поверхность выносится в public/.
Зачем: линтер и dma check строят граф по реальным путям импорта. Barrel с реэкспортами этот граф ломает.
Что проверяют инструменты: public-api, no-barrel.
Хотите разобраться глубже — Почему нет barrel-файлов.
3. Колокация по умолчанию
Новый код живёт рядом с единственным потребителем, пока не появится второй. Не создавайте shared/format-date.ts, если format-date нужен только в checkout.
Зачем: преждевременное обобщение раздувает shared/ и размывает границы модулей. Колокация держит код там, где его проще понять и удалить.
Что проверяют инструменты: частично — shared-candidate в dma doctor, когда один и тот же файл уже легально импортируют 2+ модуля (часто после перенос в shared/ или на surface services/*/public/*).
4. Правило второго использования
Выносить код на уровень выше можно, когда появился второй потребитель — не «на будущее», не «вдруг пригодится».
Типичный путь:
- Хелпер разместите рядом внутри feature A — один потребитель.
- Тот же код нужен feature B → сначала выносите (в
shared/или promotion вservices/), не deep-import из internal A. - Если B импортировал internal A напрямую —
checkупадёт наpublic-api/feature-to-featureраньше, чем doctor покажетshared-candidate. - После легального второго потребителя
dma doctorможет подсказатьshared-candidateна общем файле.
Зачем: архитектура растёт по факту использования, а не по ожиданиям.
Что проверяют инструменты: shared-candidate, предикаты входящих рёбер при promotion.
Важная деталь про типы
import type считается зависимостью так же, как обычный импорт. Типовая связь — это связь. Обходить правила через import type не получится.