Олег Марков
Принципы FSD - как проектировать архитектуру фронтенда по фичам и слоям
Введение
Feature-Sliced Design (FSD) — это подход к архитектуре фронтенда, который предлагает организовывать проект по фичам и слоям, а не по типам файлов. Вместо папок components, pages, utils и растущего хаоса вы получаете понятную структуру, где каждая часть приложения знает, что ей можно, а что нельзя.
Вам не нужно помнить десятки правил наизусть. Важно понимать несколько базовых принципов FSD и научиться применять их последовательно. В этой статье я разберу именно принципы: на чем держится FSD, почему он работает и как его внедрять без боли в реальном проекте.
Смотрите, я покажу вам:
- какие уровни (слои) есть в FSD и зачем они нужны;
- как устроена модульность по фичам;
- какие ограничения между слоями помогают не превратить проект в «спагетти»;
- как использовать сегменты (entities, features, widgets и др.);
- как описывать и применять публичные API модулей;
- как постепенно переходить к FSD в существующем коде.
По ходу статьи я буду приводить примеры структуры проекта и фрагменты кода, чтобы вам было легче связать теорию с реальной практикой.
Основные принципы FSD
Принцип 1. Архитектура вокруг фич, а не вокруг типов файлов
Классический способ организации фронтенд-проекта — по типам:
components/pages/services/hooks/utils/
Через несколько месяцев такая структура начинает плохо масштабироваться. Компоненты, относящиеся к одной бизнес-задаче, оказываются разбросаны по разным папкам. Появляются «общие» компоненты, которые на самом деле давно завязаны на конкретные фичи.
FSD предлагает другой подход: группировать код вокруг фич (функциональности), а не вокруг типов.
Как выглядит структура по фичам
Давайте посмотрим на упрощенный пример структуры на основе FSD:
src/
app/ # Корневые настройки приложения
processes/ # Долгоживущие бизнес-процессы
pages/ # Страницы (композиция виджетов и фич)
widgets/ # Крупные UI-блоки из фич и сущностей
features/ # Фичи (функции, которые есть у пользователя)
auth-by-email/
model/
ui/
lib/
change-profile/
model/
ui/
entities/ # Сущности домена (пользователь, статья и т.д.)
user/
model/
ui/
shared/ # Переиспользуемые примитивы
ui/
lib/
api/
config/
Смотрите, здесь ключевое — папки features, entities, widgets, pages. Каждая фича (например, auth-by-email) содержит все, что ей нужно:
ui— компоненты интерфейса;model— состояние и бизнес-логика;lib— вспомогательные функции.
Вы больше не ищете «где лежит логика авторизации» по трем-четырем папкам. Вы просто идете в features/auth-by-email.
Это и есть первый базовый принцип FSD:
Код группируется по бизнес-функциональности, а не по техническому типу.
Принцип 2. Деление на слои и ограничения между ними
FSD вводит несколько слоев (layers), каждый из которых имеет свою зону ответственности и свои зависимости. Это помогает ограничить «проникновение» деталей низкого уровня вверх по приложению и наоборот.
Классический набор слоев:
app— инициализация приложения;processes— кросс-страничные бизнес-процессы (опционально);pages— страницы и маршруты;widgets— крупные UI-блоки;features— пользовательские фичи;entities— бизнес-сущности;shared— переиспользуемые примитивы и утилиты.
Направление зависимостей
Направление зависимостей строго сверху вниз. Давайте разберем:
appможет зависеть от всех нижележащих слоев;pagesможет зависеть отwidgets,features,entities,shared;widgets— отfeatures,entities,shared;features— отentities,shared;entities— только отshared;shared— ни от кого из домена, только от внешних библиотек.
То есть запрещено, например:
- чтобы
entitiesзависел отfeatures; - чтобы
sharedимпортировал что-то изentitiesилиwidgets.
Это одно из ключевых ограничений FSD, которое сохраняет архитектуру чистой.
Давайте представим пример неправильной зависимости:
// ПЛОХО - сущность user тянет фичу авторизации
// entities/user/model/auth.ts
import { loginByEmail } from "@/features/auth-by-email"; // так делать нельзя
export const ensureUserAuthorized = async () => {
// ...
await loginByEmail();
};
Такой импорт приводит к тому, что базовая сущность user становится зависимой от конкретной фичи авторизации. Если вы захотите убрать эту фичу или заменить ее, придётся править базовый слой сущностей.
Как правильно:
// features/auth-by-email/model/effects.ts
// Здесь мы импортируем сущность user из entities
import { userModel } from "@/entities/user";
// Здесь мы реализуем авторизацию по email и уже работаем с userModel
export const loginByEmail = async (email: string, password: string) => {
// ...
// Здесь мы обновляем состояние пользователя
userModel.setAuthData({ email });
};
Сущности остаются «ниже» и не знают о фичах. Фичи используют сущности, а страницы собирают фичи и виджеты.
Принцип 3. Сегменты (entities, features, widgets, pages, processes)
Чтобы архитектура была не только по слоям, но и по смыслу, FSD вводит понятие сегментов. Это логические зоны приложения по отношению к пользователю и домену.
Entities — сущности домена
entities описывают предметную область: пользователь, статья, заказ, продукт и т.д. Они не знают ни о страницах, ни о маршрутизации, ни о конкретных сценариях.
Структура:
entities/
user/
model/
types.ts
selectors.ts
slice.ts
ui/
UserAvatar.tsx
UserName.tsx
lib/
formatUserName.ts
Здесь я размещаю все, что относится к сущности user:
model— состояние и бизнес-логика сущности;ui— минимальные UI-компоненты, которые показывают данные пользователя;lib— вспомогательные функции, работающие с user.
Features — возможности пользователя
features описывают то, что пользователь может делать: авторизоваться, менять профиль, лайкать статью, добавлять товар в корзину.
Структура:
features/
auth-by-email/
model/
effects.ts
slice.ts
ui/
LoginForm.tsx
lib/
validators.ts
Фича может использовать одну или несколько сущностей. Например, авторизация может работать с entities/user.
Фичи — это «действия», которые пользователь совершает в интерфейсе. Когда вы формулируете фичу, описывайте ее глаголами: add-to-cart, change-profile, rate-article.
Widgets — крупные UI-блоки
widgets — это составные блоки интерфейса, которые могут содержать в себе несколько фич и сущностей. Например:
ArticleComments— блок комментариев под статьей;UserProfileHeader— шапка страницы профиля;CartPreview— виджет мини-корзины.
Структура:
widgets/
article-comments/
ui/
ArticleComments.tsx
model/
index.ts
lib/
mapCommentToView.ts
Давайте посмотрим пример кода для виджета:
// widgets/article-comments/ui/ArticleComments.tsx
// Здесь мы подключаем сущность комментариев и фичу добавления комментария
import { CommentList } from "@/entities/comment/ui/CommentList";
import { AddCommentForm } from "@/features/add-comment/ui/AddCommentForm";
interface ArticleCommentsProps {
articleId: string;
}
export const ArticleComments = (props: ArticleCommentsProps) => {
const { articleId } = props;
// Здесь может быть логика получения комментариев по articleId
// Либо она инкапсулирована в CommentList
return (
<section>
{/* Список комментариев к статье */}
<CommentList articleId={articleId} />
{/* Форма добавления комментария */}
<AddCommentForm articleId={articleId} />
</section>
);
};
Обратите внимание, как этот фрагмент кода решает задачу: виджет собирает из сущностей и фич готовый блок интерфейса, который можно использовать на разных страницах.
Pages — страницы приложения
pages — это уроверь маршрутизации и конечная композиция. Страница собирает виджеты и фичи в конечный интерфейс.
Пример:
pages/
article-details/
ui/
ArticleDetailsPage.tsx
model/
index.ts
// pages/article-details/ui/ArticleDetailsPage.tsx
// Здесь мы импортируем виджеты и фичи, а не «глубокие» детали
import { ArticleContent } from "@/widgets/article-content";
import { ArticleComments } from "@/widgets/article-comments";
interface ArticleDetailsPageProps {
id: string;
}
export const ArticleDetailsPage = (props: ArticleDetailsPageProps) => {
const { id } = props;
return (
<div>
{/* Виджет с содержимым статьи */}
<ArticleContent articleId={id} />
{/* Виджет с комментариями */}
<ArticleComments articleId={id} />
</div>
);
};
Страницы не должны содержать сложную бизнес-логику. Их задача — сочетать уже готовые элементы.
Processes — бизнес-процессы
processes используются реже, но бывают полезны для долгоживущих сценариев, которые затрагивают несколько страниц или жизненные циклы (например, онбординг, checkout с несколькими шагами).
Если у вас нет таких сложных сценариев, вы можете начать без processes и добавить слой позже.
Принцип 4. Публичный API модулей
Каждый модуль (фича, сущность, виджет) должен иметь четко определенный публичный API — то, что можно импортировать снаружи. Это помогает избежать «протекания» деталей реализации.
Смотрите, как это выглядит на практике.
Пример: публичный API фичи
Структура:
features/
auth-by-email/
model/
effects.ts
slice.ts
selectors.ts
ui/
LoginForm.tsx
index.ts # публичный API фичи
В index.ts вы явно перечисляете, что доступно из фичи:
// features/auth-by-email/index.ts
// Здесь мы экспортируем только то, что хотим показать наружу
export { LoginForm } from "./ui/LoginForm";
export type { LoginFormValues } from "./model/types";
export { loginByEmail } from "./model/effects";
Теперь снаружи фичу используют так:
// pages/login/ui/LoginPage.tsx
import { LoginForm } from "@/features/auth-by-email";
export const LoginPage = () => {
return (
<main>
{/* Публичный компонент фичи авторизации */}
<LoginForm />
</main>
);
};
Обратите внимание: импорт идет из корня features/auth-by-email, а не из ui/LoginForm. Это важно, потому что:
- вы можете поменять внутреннюю структуру фичи;
- страница не должна знать, как именно фича устроена внутри;
- видно, какие элементы фичи являются частью ее контракта.
Пример: публичный API сущности
entities/
user/
model/
slice.ts
selectors.ts
types.ts
ui/
UserAvatar.tsx
UserName.tsx
index.ts
// entities/user/index.ts
// Здесь мы экспортируем доступные компоненты и модель
export { UserAvatar } from "./ui/UserAvatar";
export { UserName } from "./ui/UserName";
export { userReducer, userActions } from "./model/slice";
export * as userSelectors from "./model/selectors";
export type { User, UserId } from "./model/types";
Теперь вы можете использовать сущность в любом модуле, который имеет право зависеть от entities:
// widgets/user-profile-header/ui/UserProfileHeader.tsx
import { UserAvatar, UserName } from "@/entities/user";
export const UserProfileHeader = () => {
return (
<header>
<UserAvatar size="large" />
<UserName />
</header>
);
};
Здесь важно, что UserProfileHeader не лезет внутрь entities/user/model. Он работает только с тем, что явно экспортировано.
Принцип 5. Локальность и независимость модулей
Каждая фича или сущность стремится быть максимально локальной и независимой. Это означает:
- минимум внешних зависимостей;
- отсутствие «магических» глобальных импортов;
- явное указание на то, какие данные и функции нужны модулю.
Локальность состояния и логики
Смотрите, я покажу вам простой пример. Допустим, у нас есть фича «лайкнуть статью»:
features/
toggle-article-like/
model/
effects.ts
api.ts
selectors.ts
ui/
LikeButton.tsx
index.ts
// features/toggle-article-like/model/api.ts
// Здесь мы описываем работу с API для лайков
export const likeArticle = (articleId: string) => {
// Тут мог бы быть вызов fetch или axios
// return api.post(`/articles/${articleId}/like`);
};
export const unlikeArticle = (articleId: string) => {
// Аналогично здесь снимаем лайк
// return api.delete(`/articles/${articleId}/like`);
};
// features/toggle-article-like/model/effects.ts
import { likeArticle, unlikeArticle } from "./api";
// Здесь мы определяем эффект переключения лайка
export const toggleLike = async (articleId: string, isLiked: boolean) => {
if (isLiked) {
await unlikeArticle(articleId);
} else {
await likeArticle(articleId);
}
};
// features/toggle-article-like/ui/LikeButton.tsx
// Здесь мы объявляем публичный компонент для кнопки лайка
import React from "react";
import { toggleLike } from "../model/effects";
interface LikeButtonProps {
articleId: string;
isLiked: boolean;
}
// Компонент ничего не знает о глобальном состоянии приложения
export const LikeButton = (props: LikeButtonProps) => {
const { articleId, isLiked } = props;
const handleClick = async () => {
// Вызываем локальный эффект фичи
await toggleLike(articleId, isLiked);
};
return (
<button onClick={handleClick}>
{isLiked ? "Убрать лайк" : "Поставить лайк"}
</button>
);
};
Здесь вы видите, как это выглядит в коде: фича сама инкапсулирует свой API, эффекты и UI. В идеале вы можете взять эту фичу и перенести в другой проект, если доменная логика совпадает.
Локальность помогает:
- проще рефакторить отдельные фичи;
- тестировать их отдельно;
- повторно использовать в других местах.
Принцип 6. Явные границы и запреты
FSD не ограничивается только «рекомендуемыми» зависимостями. Он подразумевает явные запреты:
- нельзя импортировать из «более высокого» слоя;
- нельзя делать «сквозные» импорты, обходящие публичный API;
- нельзя из
sharedтянуть код из бизнес-сегментов.
Чтобы эти правила не оставались только на бумаге, обычно подключают линтеры (ESLint) и кастомные правила импортов. Чуть ниже я покажу типовой подход.
Пример: запрещенные импорты
Плохой случай:
// shared/ui/Button.tsx
// ПЛОХО - shared не должен знать о сущности user
import { User } from "@/entities/user";
interface ButtonProps {
label: string;
owner?: User;
}
Так делать нельзя, потому что shared лежит в основании пирамиды и должен быть свободен от доменных зависимостей. Сегмент shared — это низкоуровневые блоки: кнопки, поля ввода, хелперы, API-клиент.
Правильный подход: выносить зависимость от User наверх, например, в фичу или виджет.
Принцип 7. Внешние зависимости «прячутся» внизу
FSD рекомендует инкапсулировать использование внешних библиотек в нижних слоях, чтобы они не «расползались» по всему проекту.
Примеры внешних зависимостей:
- UI-библиотеки (
MUI,Ant Design); - клиент для запросов (
axios,fetchс оберткой); - роутеры (
react-router); - стейт-менеджеры (
Redux,Zustand,MobX); - библиотеки валидации (
yup,zod).
Идея в том, чтобы:
- вы могли заменить библиотеку в одном месте;
- верхние уровни работали с абстракциями, а не с конкретной реализацией.
Пример: обертка над API-клиентом
shared/
api/
base.ts
httpClient.ts
// shared/api/httpClient.ts
// Здесь мы создаем и настраиваем HTTP-клиент
// Допустим, это обертка над fetch или axios
export const httpClient = {
get: async <T>(url: string): Promise<T> => {
// Здесь вы можете настроить общий обработчик ошибок
const response = await fetch(url);
return response.json() as Promise<T>;
},
post: async <T>(url: string, body: unknown): Promise<T> => {
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(body),
});
return response.json() as Promise<T>;
},
};
Теперь фича или сущность используют не axios напрямую, а ваш httpClient:
// features/auth-by-email/model/api.ts
// Здесь мы используем обертку httpClient вместо прямого axios
import { httpClient } from "@/shared/api/httpClient";
interface LoginRequest {
email: string;
password: string;
}
interface LoginResponse {
token: string;
}
// Функция для авторизации по email и паролю
export const loginByEmailRequest = (data: LoginRequest) => {
return httpClient.post<LoginResponse>("/login", data);
};
Если вы захотите заменить fetch на axios, вы поменяете только shared/api/httpClient.ts.
Принцип 8. Инкрементальное внедрение FSD
FSD почти никогда не внедряют «за один раз» во взрослом проекте. Чаще всего вы:
- начинаете с выделения нескольких фич;
- постепенно переносите сущности;
- переименовываете и перекладываете папки по мере рефакторинга.
Здесь важно не пытаться переписать все сразу, а двигаться шаг за шагом.
Стратегия: с чего начать реальный проект
Давайте разберемся на практическом сценарии.
Создайте базовую структуру слоев в
src:app,pages,widgets,features,entities,shared.
Выделите пару ключевых сущностей (user, article, product) и перенесите код, относящийся к ним, в
entities.Определите несколько фич:
auth-by-email,add-to-cart,change-profileи т.п.
Постепенно переподключайте импорты через публичный API:
- вместо
../../components/LoginForm—@/features/auth-by-email.
- вместо
Включите правила линтера для импортов (например, с помощью
eslint-plugin-boundariesилиeslint-plugin-import), чтобы новые нарушения не появлялись.
Пример простого правила для путей (идейно)
Схематичный пример (идея важнее точной конфигурации):
// .eslintrc.js (идея, не полный конфиг)
module.exports = {
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
name: "@/shared",
importNames: ["SomethingFromUpperLayer"],
message: "shared не может зависеть от верхних слоев",
},
],
patterns: [
// Например, запрет прямых импортов из «глубоких» путей чужих модулей
{
group: ["@/features/*/model/*", "@/features/*/ui/*"],
message: "Используйте публичный API фичи через '@/features/feature-name'",
},
],
},
],
},
};
В реальном проекте вы можете использовать специальные плагины под FSD, которые уже знают о сегментах entities, features, widgets и т.д.
Принцип 9. Единый подход к именованию и структуре внутри модуля
FSD не жестко диктует внутреннюю структуру фичи или сущности, но рекомендует придерживаться единообразия. Это очень помогает, когда команда растет.
Часто используют следующую структуру:
ui— компоненты;model— состояние, бизнес-логика, эффекты;lib— хелперы;config— локальная конфигурация;api— запросы к бэкенду (если не вынесены вmodel).
Пример структуры фичи с пояснениями
features/
change-profile/
ui/
ChangeProfileForm.tsx # Компонент формы
model/
slice.ts # Состояние фичи (если используется Redux или аналог)
effects.ts # Асинхронные операции (запросы к API, сайд-эффекты)
selectors.ts # Селекторы, чтобы получать данные из state
validators.ts # Функции валидации формы
lib/
mapProfileToForm.ts # Преобразование доменной модели в форму
index.ts # Публичный API
Такой шаблон помогает вам и коллегам быстро понимать, где искать:
- состояние;
- эффекты;
- UI;
- вспомогательные функции.
Покажу вам, как это реализовано на практике внутри фичи:
// features/change-profile/model/validators.ts
// Здесь мы объявляем простую валидацию профиля
export const validateProfile = (data: { name: string; age: number }) => {
const errors: Record<string, string> = {};
if (!data.name) {
errors.name = "Имя обязательно";
}
if (data.age < 0) {
errors.age = "Возраст не может быть отрицательным";
}
return errors;
};
// features/change-profile/ui/ChangeProfileForm.tsx
// Здесь мы используем локальный валидатор и API фичи
import React, { useState } from "react";
import { validateProfile } from "../model/validators";
interface ChangeProfileFormProps {
initialName: string;
initialAge: number;
onSubmit: (data: { name: string; age: number }) => void;
}
export const ChangeProfileForm = (props: ChangeProfileFormProps) => {
const { initialName, initialAge, onSubmit } = props;
const [name, setName] = useState(initialName);
const [age, setAge] = useState(initialAge);
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = () => {
// Сначала валидируем данные
const validationErrors = validateProfile({ name, age });
if (Object.keys(validationErrors).length > 0) {
// Сохраняем ошибки, если они есть
setErrors(validationErrors);
return;
}
// Вызываем переданный колбек, если все ок
onSubmit({ name, age });
};
return (
<form
onSubmit={(event) => {
event.preventDefault();
handleSubmit();
}}
>
<div>
<label>
Имя
<input
value={name}
onChange={(event) => setName(event.target.value)}
/>
</label>
{/* Если есть ошибка по имени, показываем ее */}
{errors.name && <span>{errors.name}</span>}
</div>
<div>
<label>
Возраст
<input
type="number"
value={age}
onChange={(event) => setAge(Number(event.target.value))}
/>
</label>
{/* Если есть ошибка по возрасту, показываем ее */}
{errors.age && <span>{errors.age}</span>}
</div>
<button type="submit">Сохранить</button>
</form>
);
};
Эта фича уже сама по себе вполне самостоятельна. Страница или виджет могут использовать ее через публичный API.
Принцип 10. Разделение UI-композиции и бизнес-логики
Еще одна важная идея FSD — разделять:
- компоненты, которые просто отображают данные и испускают события;
- компоненты/модули, которые знают о бизнес-логике, запросах, сторе.
Внутри фичи часто выделяют «умные» и «глупые» компоненты (хотя терминология может быть другой):
- в
uiмогут быть простые компоненты, которые получают пропсы; - в
model— логика работы с данными, эффекты, интеграция со стором.
Давайте разберемся на примере лайка статьи, где LikeButton — простая «глупая» кнопка.
// features/toggle-article-like/ui/LikeButton.tsx
interface LikeButtonProps {
isLiked: boolean;
onToggle: () => void;
}
export const LikeButton = (props: LikeButtonProps) => {
const { isLiked, onToggle } = props;
return (
<button onClick={onToggle}>
{isLiked ? "Убрать лайк" : "Поставить лайк"}
</button>
);
};
// features/toggle-article-like/ui/LikeButtonContainer.tsx
// Здесь мы добавляем бизнес-логику к простой кнопке
import React from "react";
import { LikeButton } from "./LikeButton";
import { toggleLike } from "../model/effects";
interface LikeButtonContainerProps {
articleId: string;
isLiked: boolean;
}
export const LikeButtonContainer = (props: LikeButtonContainerProps) => {
const { articleId, isLiked } = props;
const handleToggle = async () => {
// Здесь мы вызываем бизнес-логику из model
await toggleLike(articleId, isLiked);
};
return <LikeButton isLiked={isLiked} onToggle={handleToggle} />;
};
Теперь вы увидите, как это выглядит: простой LikeButton можно без проблем использовать в других контекстах, а всю бизнес-логику держать в контейнере.
Заключение
Основная идея Feature-Sliced Design — построить архитектуру фронтенда вокруг фич и сущностей домена, а не вокруг технических типов файлов. Вы делите проект на слои (app, pages, widgets, features, entities, shared), задаете понятные ограничения между ними и описываете публичный API для каждого модуля.
Ключевые принципы FSD, которые стоит держать в голове:
- группировка кода по фичам и сущностям;
- иерархия слоев и направление зависимостей только сверху вниз;
- использование сегментов (
entities,features,widgets,pages,processes); - четкий публичный API модулей;
- локальность логики и состояния;
- инкапсуляция внешних библиотек на нижних уровнях;
- единый подход к структуре модулей и именованию;
- постепенное, инкрементальное внедрение.
Если вы будете придерживаться этих принципов и усиливать их линтером и код-ревью, проект станет гораздо лучше масштабироваться, а новые разработчики будут быстрее ориентироваться в кодовой базе.
Частозадаваемые технические вопросы по FSD
Как организовать тесты в структуре FSD
Обычно тесты располагают рядом с тестируемыми файлами, внутри того же модуля. Например:
features/auth-by-email/model/effects.test.ts;entities/user/model/selectors.test.ts.
Главная идея — тесты живут на том же уровне, что и код, который они проверяют. Это сохраняет локальность и облегчает навигацию. Если у вас общая папка tests, имеет смысл постепенно переносить тесты ближе к модулям.
Как быть с типами TypeScript которые используются в разных модулях
Базовые доменные типы (например, User, Article) хранятся в соответствующих сущностях: entities/user/model/types.ts. Если тип действительно общий и не относится к конкретной сущности, его можно вынести в shared/kernel или shared/types. Важно не складывать все подряд в одну «свалку типов», а сохранять привязку к домену.
Как подключать роутер в FSD структуре
Роутер обычно живет в слое app или pages:
- в
app/providers/router— конфигурация роутинга; - в
pages— компоненты страниц, которые используются в конфиге роутера.
Страницы не должны напрямую зависеть от конкретной библиотеки роутинга. В сложных проектах можно создать абстракцию роутера в shared/router и уже через нее работать с конкретной библиотекой.
Как организовать глобальное состояние в FSD
Если вы используете Redux или другой стор, обычно:
- храните конфигурацию стора в
app/storeилиapp/providers/store; - разрезаете стор по модулям — каждый модуль экспортирует свой reducer и actions через публичный API (
entities/user,features/auth-by-email); - подключаете редьюсеры к корневому стору на уровне
app.
Фичи и сущности объявляют свои слайсы и селекторы внутри model, а верхние уровни только подключают их к стору.
Можно ли использовать FSD только частично например без слоя processes
Да, FSD допускает частичное использование. Вы можете начать только с сегментов entities, features, widgets, pages и слоя app. Слой processes добавляется позже, если появляются сложные кросс-страничные сценарии. Важно сохранять основные идеи: сегменты, слои, направление зависимостей и публичные API модулей.