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

Четыре инварианта

Правила, которые 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. Правило второго использования

Выносить код на уровень выше можно, когда появился второй потребитель — не «на будущее», не «вдруг пригодится».

Типичный путь:

  1. Хелпер разместите рядом внутри feature A — один потребитель.
  2. Тот же код нужен feature B → сначала выносите (в shared/ или promotion в services/), не deep-import из internal A.
  3. Если B импортировал internal A напрямую — check упадёт на public-api / feature-to-feature раньше, чем doctor покажет shared-candidate.
  4. После легального второго потребителя dma doctor может подсказать shared-candidate на общем файле.

Зачем: архитектура растёт по факту использования, а не по ожиданиям.

Что проверяют инструменты: shared-candidate, предикаты входящих рёбер при promotion.

Важная деталь про типы

import type считается зависимостью так же, как обычный импорт. Типовая связь — это связь. Обходить правила через import type не получится.

Что дальше

On this page