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

Слои

app, features, services, shared — что это за слои и как инструменты определяют принадлежность модуля.

В DMA слой — это не звание, которое вы вешаете на папку по ощущениям. Слой — предикат: набор условий, которые можно проверить по файловой системе и графу импортов.

Если модуль попадает под предикат features/ — он feature. Появились входящие рёбра от других модулей — пора в services/. Спорить о ранге не нужно: граф уже ответил.

Дерево по умолчанию

src/
├── app/         # корень композиции
├── features/    # нет входящих рёбер от других модулей
├── services/    # есть входящие рёбра + продуктовый сценарий
└── shared/      # есть входящие рёбра + переносимый код

Папки services/ может не быть в начале проекта. Пока ни один модуль не получил входящее ребро от другого модуля, services не нужен — правила, которые на неё ссылаются, просто не срабатывают.

Предикаты слоёв

СлойУсловие
Корень композиции (app/, pages/, routes/)Собирает модули; никто из модулей не импортирует корень композиции
features/Нет входящих рёбер от других модулей (только монтирование из корня композиции)
services/Есть входящие рёбра от модулей и код реализует продуктовый сценарий
shared/Есть входящие рёбра и код можно перенести в другой продукт без изменений

Граница services / shared

Это единственное место, где остаётся человеческое суждение. Эвристика:

Может ли этот код жить в другом продукте без изменений?

  • Даshared/ (кнопка, formatCurrency, HTTP-клиент).
  • Нетservices/ (корзина, checkout-flow, права доступа к сущностям продукта).

Подробнее — в Services vs Shared.

Корень композиции

app/, pages/, routes/ под src/ — одно и то же по смыслу: точка сборки.

  • Монтирует features и services через */public/*
  • Монтирование не считается входящим ребром для promotion — роут, который рендерит <CheckoutPage />, не превращает checkout в service
  • Файлы роутов должны быть тонкими: импорт из public/, минимум логики
// app/checkout/page.tsx — тонкая оболочка
import { CheckoutPage } from "@/features/checkout/public/checkout-page";

export default function Page() {
  return <CheckoutPage />;
}

Если в проекте несколько корней композиции, предпочитайте app/; инструменты при конфликте возьмёт app первым.

Направление зависимостей

app/pages/routes  →  features/*/public, services/*/public, shared
features            →  services/*/public, shared
services            →  services/*/public, shared   (без циклов)
shared              →  shared

Запрещено:

  • любой импорт «вверх» (из shared в features и т.д.);
  • импорт внутренностей чужого модуля — только */public/*;
  • импорт feature → feature (нужен общий service или shared).

Feature не импортирует feature

Если checkout и catalog должны делить поведение — выносите общее в services/ или переносимый кусок в shared/, а не тяните импорт между features.

Promotion: feature → service

Feature с входящими рёбрами от других модулей нарушает предикат. Такой модуль переезжает в services/. Сигнал: правило feature-has-inbound.

Обратное тоже проверяется: service без входящих рёбер от модулей — service-no-inbound (демотировать или удалить).

Почему всего два слоя модулей (features + services), а не четыре как в FSD? → Почему два слоя модулей

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

ПравилоЧто ловит
layer-directionИмпорт не в том направлении
feature-to-featureFeature тянет другой feature
public-apiМежмодульный импорт мимо */public/*
no-barrelBarrel index с реэкспортами внутри модуля
no-cycleЦикл в графе модулей
feature-has-inboundFeature, от которого зависят модули
service-no-inboundService без реальных потребителей среди модулей

Полный аудит графа — dma check. Подробнее в dma check.

Что дальше

On this page