Обо мне

Начну с краткого «кто я и с какой горы припёрся?». Зовут меня Юра и у меня немногим больше семи лет опыта разработки фронта на vue+typescript в ЛАНИТ и в МТС. Начал я, что забавно, с Angular 5 в далёком 2018, когда пятёрка ещё была актуальной версией, и работал с ним немногим больше пары месяцев, после чего перекатился во vue2.

Работал я исключительно в B2B и внутренней разработке. Системы документооборота, сервисдески, внутренние ГИС и PaaS и вот это вот всё. Благодаря этому я повидал разного. От DDD, до «паста‑болоньезе‑код».

Зачем и почему эта статья

Я понимаю, что словосочетание «архитектура фронтенда» может у некоторых вызвать приступ гомерического хохота, однако вопрос насущный. Современные SPA представляют из себя комплексные приложения с большим количеством функционала и бизнес‑логики. Однако сложность приложений выросла так стремительно, что сообщество за ним не успело, и только вырабатывает лучшие практики для построения поддерживаемых и расширяемых фронтенд приложений.

Одним из последних популярных явлений фронтенд‑архитектуры стала методология FSD. Мне довелось повстречаться на ней на двух PaaS проектах и я, честно говоря, хлебнул боли. Методологию для организации кодовой базы выбирал не я, но я честно пытался в ней разобраться и как‑то удобно организовать в меру предоставленных полномочий. К сожалению, оба раза код становился проблемой с высокой связностью, неочевидным размещением сущностей и сложно отслеживаемой системой взаимодействий компонентов кода.

При всём этом наблюдаю активный рост популярности FSD методологии в сообществе. Это натолкнуло меня на мысль о том, что я, должно быть, недостаточно разобрался. А если так, значит нужно просто поглубже прокопать тему. Посмотреть, как же готовят FSD, посравнивать с известными мне архитектурными подходами. В этой статье я хочу поделиться выводами, к которым пришёл, и предложить решения, которые нашёл.

Синхронизируем понятийный аппарат

FSD (тут по причине того, что много внимания уделено критике FSD) — это архитектурная методология для проектирования фронтенд‑приложений. Из документации.

Архитектура программного обеспечения (ПО) — это план или структура, описывающая, как система организована, как разные ее части (компоненты) взаимодействуют между собой и с внешними системами. Взято отсюда

Методология разработки программного обеспечения — совокупность методов, применяемых на различных стадиях жизненного цикла программного обеспечения и имеющих общий философский подход. Взято отсюда.

Из этого выведем определение архитектурной методологии:

Архитектурная методология — это совокупность методов, применяемых при разработке архитектуры программного продукта и имеющих общий философский подход.

Прямо говоря, определение получилось несколько размытым. Всё же «совокупность методов» можно трактовать довольно широко. Поэтому я добавлю ещё одно определение: архитектурный фреймворк. Прямо говоря, прямое определение такое, чтобы его сформулировала не нейросеть, я не нашёл, поэтому предлагаю отталкиваться от определения фреймворка.

Фреймворк — программная платформа, определяющая структуру программной системы. Википедия.

Тут и добавлять ничего не нужно. Фреймворк по‑умолчанию программный инструмент, что диктует построение архитектуры по определённой методологии.

Прочие определения.

Домен — совокупность терминов, описывающих предметную область программного продукта. Для простоты будем считать доменом определения сущностей и их поведения, то есть структуры связанные с ними интерфейсы.

Так всё же методология или архитектура?

Сначала давайте подведём резюме из наших определений.

Каждое приложение имеет ту или иную архитектуру, спроектированную по определённой методологии, которую может диктовать используемый в разработке фреймворк.

Т.е. архитектура — это частный случай реализации архитектурной методологии.

Итого, FSD предлагает определённую методологию построения архитектуры приложения. Так же, как и Чистая архитектура, MVVM, DDD и прочие методологии. (вспоминает, что фреймворк — это конкретная программная реализация методологии, например flutter или spring).

Фух. С одной душной частью покончили. Поехали к следующей.

Хорошая и плохая архитектура

Раз уж мы собираемся говорить об архитектуре, то предлагаю определить, что же называть хорошей архитектурой, а что — плохой.

Для этого определим, для чего вообще нужна архитектура приложения.

Тут всё просто: архитектура приложения нужна для управления сложностью приложения. Отсюда вывод: хорошая архитектура работу над приложением упрощает, плохая — осложняет.

Очевидно, что для разных типов приложения подходят разные архитектурные подходы.

Для малых приложений и MVP — ситуаций, когда нужно быстро сделать приложение, для которого не ожидается серьёзное увеличение сложности функционала, комплексная архитектура со строгим соблюдением SOLID не то что не нужно, а может оказаться вредна и тормозить разработку: абстракции окажутся дороже самого функционала.

Для приложений с простой бизнес‑логикой и небольшим доменным пространством (например интернет‑магазины) для простоты управления сущностями уже желательно абстрагировать их от представления и разделять друг от друга. Однако абстракции вроде DI, repository всё ещё могут оказаться для таких приложений слишком неповоротливыми и неоправданно увеличивать сложность.

Для приложений с обширной доменной областью и сложной бизнес‑логикой (PaaS, ServiceDesk, Корпоративные системы документооборота) необходим архитектурный подход со строгим соблюдением SOLID. Такие приложения обычно живут долго, за время своего существования претерпевают значительные изменения как функционала, так и работающей над ним команды. При закладывании архитектуры такого приложения нужно предусмотреть вероятность смены команды, работу над ним людей разного уровня инженерной подготовки, вероятность добавления сущностей и значительного изменения существующих, вероятность добавления источников данных, изменения существующих и одновременного использования нескольких источников данных с разными парадигмами взаимодействия (REST, GraphQl, WS) и прочее, и прочее...

Ну и как же не вернуться к FSD? В конце концов именно она побудила меня к написанию этой статьи. Так давайте уделим методологии должное внимание и определим области применимости.

Область применимости FSD

Если мы говорим о малых приложениях (лендинги, PoC, MvP). Здесь FSD может иметь ограниченную применимость в минимальном своём исполнении, то есть в трёх слоях: App, pages, entity. В таком виде архитектура не вносит в работу над продуктом лишней головной боли и добавляет базовую структуру для удобства разделения инфраструктуры, бизнес‑логики и ui.

Само собой, применимо это к PoC и MVP. Лендингам оно нафиг не надо :-)

Приложения с простой бизнес‑логикой и небольшим доменным пространством

Вот где на мой взгляд идеальный «дом» для FSD. UI средней сложности легко поддаётся декомпозиции «сверху вниз», доменная модель легко укладывается в entity слой. Функции, приносящие пользователю бизнес‑ценность легко определить и выделить в features. При этом такого размера приложения редко бывают зависимы от большого количества источников данных, а само api зачастую довольно статично.

Enterprise приложения с объёмным доменом и сложной бизнес‑логикой

Это на мой взгляд тот случай, когда FSD применять не стоит. Я видел примеры попыток и это, прямо говоря, было больно.

Почему FSD плох для таких больших приложений?

  • FSD не предлагает никакой абстракции над источником данных.

    Наоборот, запросы к API идут из всех слоёв напрямую. Из‑за этого:

    • При росте приложения неизменно будет происходить дублирование кода запросов к api (спасибо «человеческий фактор»);

    • изменение и добавление новых источников данных принесёт много боли и страданий;

    • Отсутствие единой точки обработки api ошибок. Это, конечно, можно добавить в HTTP клиенте, но что, если у нас несколько источников данных, обработка ошибок от которых должна различаться? Кажется, что такое смешение ответственностей в HTTP клиенте не лучшая идея.

  • FDS осложняет декомпозицию.

    На самом деле эту претензию к FSD я слышал чаще всего. Да и сам её заметил. Когда приложение перешагивает определённый порог сложности, становится очень сложно определить, в какой слой сложить тот или иной функционал. Чаще всего путаница случается между слоями Widgets и Features, но ими не ограничивается. И чем больше команда, тем больше путаницы происходит. Отчасти это можно исправить строгой документацией, регламентирующей правила декомпозиции, но полностью оно проблему не решит.

  • FSD порождает высокую связность.

    Да, я знаю, что FSD как раз старается избежать сильных связей между сущностями. Он гарантирует независимость сущностей внутри слоя и постулирует инкапсуляцию реализации, при которой взаимодействие между слоями ведётся только через PublicApi. Но чем выше по уровням приложения мы поднимаемся, тем сильнее будет связность.

    И однажды, когда вам понадобится переместить компонент с уровня widget на уровень features вы проклянете всё. И проблема не в том, что придётся исправлять импорты. Это можно сделать самостоятельно. А в том, что ваш компонент зависел от трёх фич, которые вы больше не можете использовать, потому что импорт между слайсами внутри одного слоя запрещён. А значит вам нужно либо перемещать фичи ниже (что гарантированно приведёт к конфликтам уже на уровне entity), либо дублировать код. Гарантирую, вы предпочтёте просто продублировать код.

    На последний пункт можно возразить: «ты просто изначально неправильно разделил сущности». И да, это одна из потенциальных причин. Но вспомните предыдущий пункт и то, что в FSD бывает очень не просто правильно разделить сущности по слоям.

    Однако есть и вторая причина: доработка функционала.

    Не редки случаи, когда изначальные требования предполагали функционал в wingets, но для адаптации под новые требования тот же функционал приходится переместить на слой ниже. Всё же разработка штука динамичная и редко когда можно предсказать, что добавится в приложение через год.

Критикуешь, предлагай

На самом деле всё уже давно придумано до нас. Однако я в статье пойду последовательно. И начну с определения компонентов, из которых состоит любое крупное frontend приложение.

  • UI — то, что пользователь видит в браузере

    • Вьюхи — конечные страницы на url, которые

    • виджеты (группы компонентов, объединённые общим функционалом)

    • компоненты (компоненты, отвечающие за изолированный функционал)

  • Бизнес‑логика (use cases)

    • Реализация сценариев использования пользователем приложения

    • Хранилище бизнес‑сущностей (store)

  • Модель данных (Domain layer)

    • Бизнес — бизнес‑сущности, описанные в TS типах

    • Репозитории — унифицированные интерфейсы, между коннекторами и view контроллерами, оперирующие бизнес‑моделями.

  • Инфраструктура

    • DI container;

    • command/query bus;

    • контроллер управления модальными окнами;

    • контроллер управления тостами и так далее..

  • Коннекторы (источники данных)

    • Запросы к api (axios/fetch/tanstack)

    • auth провайдеры (google auth/yandex auth)

    • indexdb

Для полноценного абстрагирования, как мне кажется, в этом списке не хватает только одного пункта: Repositories — который будет реализовывать абстрактные провайдеры и брать на себя функцию брокера данных. Я расскажу о слое репозиториев подробнее, когда буду говорить конкретно о каждом слое приложения.

Посмотрим на каждый слой пристальнее

Ниже будут примеры кода. Я намерено не буду углубляться в конкретные детали реализации, сосредоточившись на общей структуре и правилах взаимодействия компонентов. Считайте их абстрактными примерами.

UI

Представление бизнес‑области, с которым взаимодействует пользователь. UI слой взаимодействует только с бизнес‑логикой и моделью данных. И больше ни о чём в приложении он знать не должен.

Для удобства работы UI слой разумно разделить более тонко по уровню сложности элементов. Например так:

  • pages — страницы, завязанные на конкретные роуты;

  • views — умные компоненты, использующие несколько виджетов и обращающиеся к слою бизнес‑логики;

  • wigets — глупые композитные компоненты (объединяющие в себе несколько components);

  • components — глупые компоненты, реализующие атомарную функциональность.

Предложенное выше разделение не догма, а только пример.

Важно держать UI слой чистым от реализации бизнес‑логики, а компонентный слой типизировать интерфейсами взаимодействия, описываемыми в слое модели данных.

<template>
  <UserSimpleView :user="user" :loading="loading" /> <!-- Принимает интерфейс User из @/domain/models/user -->
  <MyButton :disabled="loading" @click="handleGetUser">Загрузить</MyButton> 
</template>
<script setup lang="ts">
import { useGetUser } from '@/use-cases/user'

const { uuid } = defineProps<{ uuid: string }>()
const { user, getUser, loading } = useGetUser()

// Максимально просто. Вся бизнес-логика в use функции.
function handleGetUser() {
  getUser(uuid)
}
</script>

Бизнес-логика (use-cases)

Слой, на котором обрабатывается взаимодействие пользователя с данными и хранятся эти самые данные. То, что FSD называет фичами.

  • Бизнес‑логика ничего не знает о том, что её будут применять в UI и ничего не знает о том, откуда берутся данные и кем и как они меняются.

  • Бизнес‑логика типизируется из слоя domain.

  • Бизнес‑логика получает данные из repositories, но ничего не знает о том, откуда сами repositories получают данные, и как они их обрабатывают.

Таким образом слой бизнес‑логики получается независим от источников данных, от конкретной реализации инфраструктуры и от UI.

Внутри можно разделять как удобно. К примеру по доменной области.

Тут прекрасно ложится взаимодействие между компонентами через publicApi, типизированное доменным слоем. Это позволит стандартизировать контракты взаимодействия и уменьшить степень связности.

Немного подумав, вижу, что бизнес‑логика также хорошо разделяется на три‑четыре слоя логики с принципом однонаправленного потока данных из FSD.

// описание логики конкретного use-case — запроса данных о пользователе.
// Use case не должен знать ничего о коннекторе, который фактически лезет в api,
// вместо этого он обращается к абстрактному провайдеру из domain слоя.
export function useGetUser() {
  // Контейнер репозиториев лишь пример реализации инверсии зависимостей.
  // Не обязательно использовать именно его. 
  // Отталкиваться стоит от специфики вашего приложения.
  const repository = useRepositoryContainer().getRepository(Symbol("GetUsersByApi"))
  const user = ref<User | undefined>()
  function getUser(userUUID: string) {
    const query = {
       // логика формирования запроса
    }
    const result = repository.getSingleUser(query)
    // предполагаем, что пользователь возвращается единственным элементом в массиве
    user.value = result [0]
  }
}

Repositories

Задача репозиториев в том, чтобы предоставить слою use‑cases предсказуемый интерфейс для получения данных. Для этого слой должен реализовывать интерфейсы взаимодействия из domain слоя.

Репозитории забирают данных из коннекторов, адаптируют их ко внутренним типам и интерфейсам и предоставляют по требованию. Тут находятся все мапперы, происходят первичные обработки ошибок, настраиваются политики кэширования, и тут же хранится состояние нашего приложения.

На самом деле, при большом желании store можно вынести отдельным модулем на уровень провайдеров, но я не очень представляю, зачем. Внутри репозиториев он выглядит намного органичнее, логичнее и удобнее.

// Простой пример реализации репозитория.
// Под капотом может быть Tan stack, Pinia и в целом что удобно вам,
// и что наилучшим образом подходит вашему приложению.
export function userRepositoryBuilder(connector: AbstractConnector): UserRepository  {
  return () => (
    getUsers: (query: GetUsersQuery) => { /* ... */ }
    getSingleUser: (query: GetSingleUserQuery) => { /* ... */ }
    createUser: (command: CreateUserCommand) => { /* ... */ }
    // реализация остальных методов интерфейса репозитория
  )
// Простой пример инициализации репозитория в DI контейнере.
// Как я говорил выше, DI контейнер можно заменить на более удобную вам реализацию.
import useRepositoryContainer from '@/infrastructure/repositoryContainer'
import userRepositoryBuilder from '@/repositories/userRepository'

// Использование контейнера опционально, но удобно для синглтон репозиториев. К тому же
// контейнер можно типизировать и получать на метод getRepository автокомплит со
// списком существующих в контейнере репозиториев.
useRepositoryContainer().registerRepository(Symbol('UserRepository'), userRepositoryBuilder())

Domain

Слой, отвечающий за данные: бизнес‑модели и контракты взаимодействия элементов системы. По сути, он синхронизирует контракты взаимодействий для всей бизнес‑логики приложения.

// src/domain/user/model
export interface User {
  uuid: UUID
  name: string
  email: string
  // прочие поля
}
// src/domain/user/abstractRepository
export interface UserRepository {
  getUsers(query: GetUsersQuery): User[]
  getSingleUser(query: GetSingleUserQuery): User
  createUser(command: CreateUserCommand): CommandResult<User>
  updateUser(command: UpdateUserCommand): CommandResult<User>
  deleteUser(command: DeleteUserCommand): CommandResult<Boolean>
}

Infrastructure

Тут все вспомогательные инструменты для функционирования приложения. Реализации контроллеров, шин, инициализация сторов. Всё это тут.

Connectors

По сути, это самый низкий и самый критичный к абстрагированию слой. Данные влияют на всё приложение. При неправильной работе с данными и их адаптации любое изменение api контрактов может повлиять на всё приложение. Задача архитектуры приложения сделать так, чтобы этого не произошло.

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

Именно для этого между коннекторами и бизнес‑логикой мы помещаем слой провайдеров, зафиксированный AbstractRepositories контрактами из домена.

Схема зависимостей

Ниже прикладываю схему зависимостей, где стрелками указаны направления зависимостей.

Зелёным обозначен бизнес-слой, серым — слой работы с данными
Зелёным обозначен бизнес‑слой, серым — слой работы с данными

Видно, что от инфраструктурного слоя зависит всё приложение, однако это набор независимых вспомогательных инструментов. Так, для ui инфраструктурный слой может реализовать систему управления тултипами и модальными окнами, для репозиториев — обработчик ошибок, для use cases шину событий.

Самое главное, что слой домена полностью независим.

UI зависим только от домена и от use‑cases, который, по сути, представляет из себя view‑model.

Use cases зависят от Repositories как от основного источника данных. По сути, Repositories представляет из себя model из архитектуры MVVM. Use cases и repositories стандартизируют общение друг с другом посредством интерфейсов, декларированных в domain.

Repositories, зависит от коннекторов и абстрагирует их от всего остального приложения, адаптируя согласно интерфейсам взаимодействия, определённым в слое домена.

Коннекторы могут быть независимы полностью, либо зависеть от инфраструктуры а части, например, http клиента.

Теперь сверим получившуюся архитектуру с SOLID

  • Единая ответственность: каждый слой приложения отвечает за что‑то одно: получение данных, адаптация данных, источник контрактов, бизнес‑логика. Внутри слоя более точечное разделение зон ответственности реализуется в зависимости от слоя.

  • Принцип открытости закрытости: каждый слой легко структурировать пирамидально, от более абстрактных компонентов/классов/функций к более конкретным. Тут можно использовать удобный вам механизм: композиция, наследование...

  • Принцип подстановки Барбары Лисков: больше про конкретные реализации и декомпозицию типов. Однако благодаря выделению домена, мы получаем единую точку истины по типам — удобнее типизировать и управлять контрактами.

  • Принцип разделения интерфейса: декларируем интерфейсы в доменном слое и сепарируем их по бизнес‑сущностям.

  • Принцип инверсии зависимостей: каждый из слоёв абстракции имеет зависимость только от прямого соседа и от домена, реализующего единую типизацию для всего приложения. Подключение модулей через интерфейсы даёт возможность легко их заменять или подменять.

Тестирование

Тут много говорить не буду.

Я считаю, что самое главное покрыть тестами бизнес‑логику, и в предложенной структуре приложения сделать это достаточно просто. Бизнес‑логика зависит только от репозиториев, которые достаточно просто подменить моками и убедиться, что всё работает как надо.

Сложнее будет с UI. Но с ним всегда сложнее. Благо, в предложенной реализации нет зависимости UI от сторов и не придётся мучиться с тем, чтобы мокнуть импорт вызовы Пиньи. Достаточно подменить реализации use‑cases и ненужные для теста компоненты.

Ограничения

Первое, что бросается в глаза — большое количество абстракций. Далеко не всегда они нужны в таком количестве. Если приложение не подразумевает большое количество логики на фронте или если доменная зона относительно простая, то предложенные мной абстракции будут излишни и с большой долей вероятности внесут в приложение неоправданное усложнение.

Построение подобной архитектуры может потребовать определённых компетенций от разработчиков. При недостаточном понимании, что и для чего делается, предложенная мной структуры может очень осложнить жизнь.

Методология не подойдёт, если вам нужно быстро показать рабочее приложение. Старт у неё будет заметно дольше, чем у fsd в её минимальном исполнении.

В заключение

Очевидно, что предложенная мной архитектура, которая, по сути, представляет из себя переложение на frontend чистой архитектуры Роберта Мартина, далеко не универсально. Я пришёл у ней, когда пытался отрефлексировать боль от использования FSD в разработке крупных и комплексных приложений.

Я не говорю о том, что FSD однозначно плоха. Вовсе нет. У неё есть своя зона применения, где она удобна. Помимо этого авторы применяют ряд отличных методов, которые можно применять в других архитектурах для их усиления, увеличения гибкости или удобства работы с ними.

Я лишь призываю подходить с умом к выбору архитектурного фреймворка или паттерна и если вы решили использовать FSD, потому что слышали, что она хороша. Возможно стоит подумать ещё раз, а точно ли она отвечает нуждам именно вашего продукта.

Могу сказать, что в будущем, если мне придётся разрабатывать крупное и сложное приложение, проектировать его по методологии, которую описал в этой статье.

PS: обоснованная критика и дискуссия приветствуется. Если нашли ошибки или опечатки — говорите. Я исправлю :-) Если Хабр позволяет... а то первая статья, фукнционал ещё не знаю

Комментарии (49)


  1. dealenx
    16.11.2025 17:42

    Я пробовал использовать FSD, в целом не так и плохо, но в какой-то момент каждый проект становится лень настраивать, особенно когда создаешь куча небольших проектов с фронтендом.

    Как бы вы назвали предложенный подход?


    1. I0rrik Автор
      16.11.2025 17:42

      Ну для относительно небольших проектов fsd весьма не плох. Вполне нормальный способ организации кодовой базы.

      Проблемы начинаются, когда проект большой :)

      Предложенный мой подход незачем как-то называть. Это, по сути, чистая архитектура, адаптированная под фронт


      1. artptr86
        16.11.2025 17:42

        Что же делать, когда маленький проект на FSD начинает разрастаться?


        1. I0rrik Автор
          16.11.2025 17:42

          Я думаю, что нужно мигрировать, пока проект не превратился в свалку :)

          Я бы начал с выделения доменного слоя, потому что от него потом будет зависеть всё.

          Потом я бы выделил ui слой, чтобы отвязать представление от бизнес-логики.

          После я бы постепенно перетащил api в слой коннекторов. Важно сразу к каждому коннектору делать обвязку-репозиторий. Поэтому переход и постепенный. Вынесли один api коннектор => сделали к нему реализацию репозитория, занялись следующим api коннектором. Если в FSD слайсах изначально разделяли бизнес-функции и api обращения, это пройдёт легче.

          В итоге в FSD слоях естественным образом должна остаться только бизнес-логика. Её останется только сгруппировать в отдельном слое приложения.

          В финале доработать инфраструктурную обвязку в случае необходимости.


      1. dsrk_dev
        16.11.2025 17:42

        Кстати в последнрей версии поправили главную боль малекньких проектов. Теперь если что-то используется только на одной странице, это можно держать прям в этой странице и не размазывать по слоям. А на слои нужно выносить только в случае если сущность переиспользуемая


  1. bighorik
    16.11.2025 17:42

    Про причины популярности фсд я бы дополнил: когда я года два назад гуглиларо архитектуры на фронте, ничего кроме фсд он мне предложить и не смог. Вместе с тем лично я эту архитектуру так понять и не смог, из описаний как будто следовало, что я всю фигню кладу в кучу, но делю по размеру. Пониманию так же не способствовали срачи мейнтейнеров в из чате на тему "куда положить компонент N"

    Мне ваша статья показалась весьма интересной, как раз в обозримом будущем мне предстоит пилить относительно большой фронт, и у меня предостаточно времени, чтобы сразу сделать по фен-шую) Полагаю, ваша статья ляжет в основу.

    Буду признателен, если вы сможете запилить минимальный туду лист с данной архитектурой для наглядности


  1. deepmind7
    16.11.2025 17:42

    Есть несколько интересных идей. Но в целом подход похож на джаву, такой же громоздкий и перегруженный. Меньше всего такое хочется видеть на фронте.


    1. I0rrik Автор
      16.11.2025 17:42

      Да, примерно как в Джава или в Шарпах. Хотя в Рельсах, помнится, тоже похожий подход.

      Все они не просто так к нему пришли. Когда монолит разрастается, за управляемость и предсказуемость кодовой базы приходится платить определённую цену. Как по мне, лучше так, чем то, во что превращается fsd на сложных проектах через полтора-два года разработки.


    1. artm_frolov
      16.11.2025 17:42

      В последнее время на фронтенде как раз таки приходится видеть жесткий перегруз, я уже даже не особо различаю эту грань между фронтендом и бэкендом


  1. ogregor
    16.11.2025 17:42

    На практике, если команда меняется и нет сильного контроля за PR, то FSD превращается в помойку и не ясно уже где искать widgets, features, e t.c


    1. karminski
      16.11.2025 17:42

      Точно также можно сказать про любую архитектуру. Контроль и чётко описанные правила кода обязательно должны быть доступны команде разработки.


      1. I0rrik Автор
        16.11.2025 17:42

        Естественно.

        Энтропии подвержена любая кодовая база, как бы пристально за ней не следили. Мы можем только влиять на величину энтропии во времени :)

        Две большие проблемы FSD на мой взгляд:

        1. Очень свободная трактовка принципов раскладывания сущностей по слоям

        2. Сваливание в одну кучу разнородных этапов работы с данными, то есть мы на одном слое абстракции и получаем данные, и обрабатываем их, и обсчитываем бизнес-логику, и готовим UI. Это порождает сильную связность между этапами процесса и больно аукается со временем.


  1. kapitan7577
    16.11.2025 17:42

    Спасибо за статью. За многие годы боли от FSD пришел примерно к такой же структуре, но с небольшими отличиями.

    1. Приложение делится на слои

      • app — основной слой приложения, используется для сборки конечного результата.

      • shared — shared слой, как и в FSD, должен быть полностью независимым и легко может быть вынесен в отдельный пакет, например.

      • catalog / cart и т.п. другие слои приложения, в рамках FSD это можно назвать фичами.

    Каждый из слоев делится на слайсы в рамках чистой архитектуры:

    • domain

    • application

    • infrastructure

    • presentation

    • integration

    Здесь все так же, как у вас, за исключением:

    • Слой application, а именно use-case, в вашем случае сейчас зависит от инфраструктуры (ref), что нарушает принципы чистой архитектуры. Для себя же принял решение, что use-cases должны содержать только бизнес-логику и зависеть только от доменного слоя, поэтому в моем случае use-case будет вида:

      JavaScript

      GetUserUseCase => {
          // ...
          return repository.getSingleUser(query)
      }
    • А вот в слайсе presentation можно реализовать уже composable useGetUser, который будет обращаться к use-case и оборачивать результат в ref. В простом случае для Nuxt-приложения этот composable будет оберткой над useAsyncData, в котором вызывается данный use-case.

    Этими шагами удается полностью изолировать бизнес-логику от UI-слайса, и если когда-то будет принято решение переехать с nuxt на что-то другое, то достаточно будет переработать только слайс ui.

    В целом сам слайс ui делится на:

    • components — глупые компоненты

    • widgets — компоненты с частью БЛ

    • pages (только если есть страницы, где используются только данные текущего слоя)

    Как и в FSD, кросс-импорты между слоями запрещены, для взаимодействия между ними используется слайс integration.

    В каждом слое в доменном слое определяются свои доменные модели, и даже если они частично совпадают (например, Catalog\Product и Cart\Product), это будут отдельные модели со своими полями. В слайсе integration мы можем обратиться к другому слою и вернуть что-то, что необходимо нам.

    Например, в слое Cart определяется порт GetProductData(productId: number), а реализация этого порта находится в integration, где происходит вызов ProductRepository и трансформация Catalog\Product в Cart\Product. Этим мы добиваемся того, что модели в разных слоях могут развиваться по своему, и не нужно сильно связывать их.

    Также в качестве DI-контейнера использую Inversify, для его поддержки в каждом слое есть bootstrap.ts, в котором выполняю регистрацию всех зависимостей для DI.

    Выполняя разделение таким образом, становится возможным вести разработку каждого слайса независимо от других. Если говорить в рамках Nuxt, то можно использовать nuxt/layers под каждый слой и в целом выносить их в отдельные репы (если слой очень большой).

    Слой app используется для конечной сборки приложения, здесь вся работа фактически выполняется в UI-слайсе, который делится:

    • pages — чистые компоненты страниц, у них есть свои пропсы и т.п. Здесь могут быть использованы компоненты с разных слайсов.

    • routes — здесь определяются маршруты приложения, внутри вызываем компоненты из Pages. В рамках Nuxt это файловая маршрутизация, но может быть заменена и на routes.ts определение маршрутов.

    • layouts


    1. I0rrik Автор
      16.11.2025 17:42

      По поводу независимости бизнес-логики от инфраструктуры:

      Это, получается, всё состояние хранится в presentation? А что делать, если его нужно шарить между представлениями? А что делать, если бизнес-логика соседнего раздела зависит от этого состояния? Кажется, что появляется двунаправленная связность бизнес-логика <=> UI. А это, кмк, нехорошо.

      Другой вариант — каждый раз высчитывать то же состояние заново, что тоже не выглядит удобным решением.

      Ну либо реактивность состояния достигается не средствами фреймворка. Я недопонимаю, как именно это работает, честно говоря. Можно подробнее раскрыть тему?

      Как и в FSD, кросс-импорты между слоями запрещены, для взаимодействия между ними используется слайс integration.

      Вот о том, что выше, тоже можно подробнее? Любопытно, как именно это реализовано.

      Например, в слое Cart определяется порт GetProductData(productId: number), а реализация этого порта находится в integration, где происходит вызов ProductRepository и трансформация Catalog\Product в Cart\Product. Этим мы добиваемся того, что модели в разных слоях могут развиваться по своему, и не нужно сильно связывать их.

      А в чём драматическое различие продуктов в Cart и в Catalog? Ну, то-есть, не разумнее ли сделать

      interface Product { ... }
      
      interface CartProduct extends Product { ... }
      
      interface CatalogProduct extends Product { ... }

      Так проще сохранить консистентность домена, отслеживать связи домена и можно использовать полиморфизм, то есть условно так:

      function doSmth<T extends Product>(product) { ... }
      
      dosmth(cartProduct)
      dosmth(catalogProduct)

      Собственно это одна из важным причин выделения домена самостоятельным слоем.

      Вторая важная идея — по единому, не размазанному по слоям, домену можно быстро составить представление о бизнес-области всего приложения и сориентироваться о взаимосвязях бизнес-сущностей.


      1. kapitan7577
        16.11.2025 17:42

        1. А в чём драматическое различие продуктов в Cart и в Catalog? 

          В том, чтобы 2 этих слоя не были зависимы друг от друга. Например (очень грубый, но понять о чем я поможет). Изначально каталог содержал товары без вариаций, и ее модель была такая

          Catalog\Product { name: string } .

          Соответственно в корзине нам тоже достаточно просто названия товара Cart\Product { name: string } .

          С ростом каталога, приходим к выводу, что нужны вариации и модель трасформируется в

          Catalog\Product { name: string, sku: ProductSku[] }

          Если не разделять модели, то изменения в каталоге неизбежно будут «просачиваться» в корзину. Если разделять, то модели развиваются независимо, и при изменениях в каталоге достаточно подправить адаптер:

          Catalog\ProductSku -> Cart\Product

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

          Например, если в каталоге появится ещё одна сущность, которую можно положить в корзину, то просто добавляется ещё один порт для её трансформации. Никакого влияния это не окажет на существующие сущности Cart.

        2. Вот о том, что выше, тоже можно подробнее? Любопытно, как именно это реализовано

          У себя это реализовываю создавая в application слое Port , например

          // application/ports/GetProductPort

          interface GetProductPort { getProduct(productId): Cart\Product }

          соответственно в integration лежит реализация этого интерфейса

          // integration/catalog/GetProductAdapter

          class GetProductAdapter implements GetProductPort

          {

          getProduct(id: string): Cart\Product {

          return this.transform(this.productRepository.findById(id));

          }

          }

          и далее этот адаптер получаю через DI в use-case'ax

        3. Что касается состояния, то здесь я разделяю его на 2 вида

          • Хранится в ui/stores, например в Pinia.
            Это чисто визуальное/вспомогательное состояние:

            • открытые модалки

            • выбранные фильтры

            • текущие вкладки

            • временные поля формы

            UI state никогда не используется в domain или application.

          • Это состояние уровня application, влияющее на выполнение use-case’ов.

            Здесь я использую RxJS (но подойдёт любой механизм реактивности, который не зависит от конкретного UI-фреймворка — Vue/React/Angular и т.п.).

            Доступ к AppState также вынесен через порты, а реализации этих портов находятся в integration, точно так же, как и адаптеры для работы с внешними сервисами.


        1. I0rrik Автор
          16.11.2025 17:42

          Если что, я не призываю бежать переписывать проект :)
          Мне интересно поразмышлять на тему в качестве тренировки.

          1. При выделении обощённого интерфейса для наследования, дочерние сущности останутся друг от друга независимыми (они ничего не будут знать о существовании друг друга) и всё так же можно будет для каждого отдельного типа продукта создавать собственный адаптер, а добавить новый продукт будет не сложнее.

            Но можно будет создавать обобщённые функции при необходимости и можно будет проследить дерево наследования, чтобы понять общую бизнес-функциональность.

            При нынешнем же подходе высок риск того, что необходимо будет местами дублировать код, потому что, хоть сущности и разные, но они однородны, то-есть бизнес-сущность у них общая, а значит с точки зрения бизнеса есть и общие ступени обработки этих сущностей в процессах.

          2. Немного похоже на костыль, чтобы обойти ограничение FSD на кросс-импорты. Хороший костыль, но всё ещё костыль.

            Кажется, что удобнее было бы разложить сам бизнес-слой на fsd и уже внутри этих слоёв запретить кросс-импорты. Тогда однонаправленный поток импортов лёг бы намного удобнее. А при необходимости спустить какую-то функцию на слой ниже можно даже безболезненно выделить новый слой, чтобы не мучиться с адаптацией импортов. Но с выделением нового бизнес-слоя нужно быть очень осторожным и прикинуть, а логично ли это, и не окажется ли это слой для одного единственного кейса.

          3. Я думал об этом, но кажется странным решением, честно говоря. Да, так ослабляется зависимость от react или vue, но добавляется новая зависимость, увеличивается размер бандла и порог вхождения в проект (особенно если говорить об Rx). Стоит ли игра свеч?

            То, что ты обозначаешь как ui состояние я бы у себя реализовал отдельными инфраструктурными сервисами. Навроде "ModalService", который бы управлял работой модальных окон. Но пока не уверен, что это закрыло бы все потребности ui.


          1. kapitan7577
            16.11.2025 17:42

            1. А для чего нужен общий интерфейс?

            В моем понимании он необходим в случаях когда, есть какой-то общий код который будет работать с этими сущностями,

            либо же сами сущности являются видами одного и того же. Например возьмем тот же пример с оружием, есть метод

            shotWeapon(weapon: Weapon)

            и интерфейсы

            interface Weapon { shot() }

            interface Gun extends Weapon

            interface Rifle extends Weapon

            здесь наследование вполне приемлемо, т.к с помощью него мы скрываем за абстракцией конктерные реализации.

            если же рассматривать текущий пример (наследование между слоями), то фактически CartProduct и CatalogProduct это не части чего-то общего,

            у них совершенно различные назначения, поэтому обьединять их нельзя.

            Да, на начальных этапах кажется, что это одно и тоже, и есть желание их объединить, однако, если проект долгоживущий, то развитие этих частей по отдельности даст больше преимуществ.

            Например добавление новой сущности к продаже - цифровая подписка, сразу ломает это наследование, т.к это уже не продукт, но в тоже время может находиться в корзине наравне с другими товарами.

            И что касается примера с оружием, я допускаю, что где-то у нас точно будет наследование, но скорее всего оно будет находится в рамках одного слоя, например тот же

            CatalogProductSimple extends CatalogProduct

            CatalogProductSku extends CatalogProduct

            2. Это стандартная практика в гексогональной архитектуре, здесь же это ее вариация.

            Разделение на слайс integration через порты и адаптеры позволяет каждый слой разрабатывать и тестировать не зависимо от других, при разработке корзины не нужно дожидаться когда окончательно будут доступны модели каталога и ее можно вести параллельно самому каталогу.

            А для тестирования достаточно будет просто замокать зависимости от сюда и вся БЛ самого слоя станет доступной для тестов.

            3. В нашей практике как показал опыт это достаточно удобно, но опять же я не призываю использовать именно rxjs.

            Можно хоть реализовать свою вариацию на обсерверах ил подобном. Но в целом, если перехода на другую платформу не предполагается, то можно договориться и об использовании тех же ref, reactive, pinia не только в слайсе ui.


            1. I0rrik Автор
              16.11.2025 17:42

              1. Моё предположение об общности CartProduct и CatalogProduct проистекает из незнания бизнес-области продукта и семантической общности сущностей.

              2. Да, только с нюансом, что в гексагональной архитектуре порты используются для подключение внешних сервисов, а не для соединения сущностей внутри бизнес-логики.

                То есть, чтобы обойти ограничения FSD тебе пришлось сделать вид, что элементы твоей бизнес-логики друг для друга внешние модули. Я это имею в виду костылём.

              3. Мне очень нравится идея ограничить применение SDOM библиотеки только ui слоем, но очень не нравится идея тащить сторонний механизм реактивности. А городить самописную реактивность очень своеобразная задача, которая, прямо говоря, большинству проектов, даже крупных, совсем не нужна. При этом смена SDOM библиотеки, прямо говоря, в любом случае встанет в немалые трудозатраты. Даже если ограничить применение только UI слоем. Так что пока что, издалека, мысль идея хоть и приятная, но как будто бы оверинжиниринг и создание себе проблем на пустом месте.


              1. kapitan7577
                16.11.2025 17:42

                Именно. Я предлагаю рассматривать взаимодействие между слоями так же, как взаимодействие между внешними модулями. И в частности поэтому модели стоит разделять и не смешивать их внутри доменной логики.


                1. I0rrik Автор
                  16.11.2025 17:42

                  Я одной из важных задач доменного слоя (да и в целом архитектуры) вижу документирование приложения. Исходя из этого: архитектура должна строиться так, чтобы можно было легко найти однородные сущности и понять, в чём их общность. Часто этого можно добиться благодаря выстраиванию правильной иерархии сущностей через наследования и абстрактные поведенческие интерфейсы.

                  Ещё один важный принцип формирования домена сервиса — семантическая консистентность.

                  Что это значит

                  Если бизнес выделяет сущности CartProduct и CatalogProduct, то, следуя из семантики, можно сразу понять, что это родственные сущности. Они могут существовать, ничего друг о друге не зная, но у них в доменном слое должен быть общий предок, через которого ясно, в чём именно состоит общность, а в чём они различны.

                  Иначе говоря, если эти сущности друг от друга абсолютно независимы, то называться они должны так, чтобы мы из семантики не могли предположить их общность. К примеру CatalogItem и CartProduct.

                  Для чего это нужно?

                  Уменьшение когнитивной сложности приложения. Чем точнее мы мы разделяем сущности семантически, тем проще в них ориентироваться и проще погружать новых разработчиков в продукт. А в идеале, если синхронизировать терминологическое поле между фронтендом, бэкендом и бизнесом, то намного проще общаться с бэком и бизнесом: эффективность коммуникации заметно возрастает.

                  Так что правильное формирование доменной модели не стоит недооценивать.


        1. Femistoklov
          16.11.2025 17:42

          Правильно понимаю, вы, вместо того чтобы БЛ-состояние хранить, применяя механизмы фреймворка, и напрямую использовать в UI, держите его в RxJS-объектах и прокидываете через прослойки, где мэппите одни события обновления в другие события обновления?


          1. kapitan7577
            16.11.2025 17:42

            Да, что-то вроде этого. Но важно понимать, что мы так делаем только для состояния, которое используется в бизнес-логике (например, в use-case’ах), а не для UI-only state.

            например

            // order/application/ports/AuthPort

            interface AuthPort {

                getUserDataStream(): Observable<UserDto | null>

                getCurrentUser(): UserDto | null;

            }

            // order/integration/Auth/AuthAdapter

            class AuthAdapter implements AuthPort {

                getUserDataStream(): Observable<UserDto | null> {

                    return authState$.pipe(

                        // ...

                    );

                }

                getCurrentUser(): UserDto | null {

                    const state = authState$.getValue();

                    return state;

                }

            }

            тогда в БЛ используем напрямую этот порт, а если состояние нужно в UI то делаем composable

            function useAuthData()

            {

                const userData = ref<UserDto | null>(AuthAdapter.getCurrentUser());

                onMounted(() => {

                    const subscription = AuthAdapter.getUserDataStream.subscribe(data => ref.value = data);

                    onUnmounted(() => {

                        subscription.unsubscribe();

                    });

                });

                return { userData }

            }

            это утрированный пример, но принцип понятен.


            1. Femistoklov
              16.11.2025 17:42

              const subscription = AuthAdapter.getUserDataStream.subscribe(data => ref.value = data);

              Тут производительность не может пострадать из-за постоянных переприсваиваний и копирований в случае больших и/или вложенных объектов, особенно с учётом того, что на данные навешивается реактивность фреймворка?

              function useAuthData()

              Это только для чтения, а как решаете, если состояние в бизнес-логике надо поменять из UI?


  1. Unnemed112
    16.11.2025 17:42

    Для малых проектов fsd это оверхед, для больших превращается в один большой головняк из за протекания абстракций и дублирования кода. Создаётся впечатление, что fsd придумали просто потому что нужно было что то придумать, для людей которые с трудом осилили(или нет) документацию своего стека, что бы они могли почувствовать себя архитектороми приложениий


    1. I0rrik Автор
      16.11.2025 17:42

      Ну для средних проектов FSD вполне может быть применим.
      Да и для малых ок в минималистичном исполнении из трёх слоёв. Просто для минимальной структуризации кода.

      Ну, я считаю, что для интернет-магазинов или личных блогов самое вот оно.


      1. Unnemed112
        16.11.2025 17:42

        Не смогу с вами согласиться, тк был опыт на разного масштаба проектах. Везде были проблемы и проблемы разные.


        1. I0rrik Автор
          16.11.2025 17:42

          О средних проектах моё предположение умозрительно. Я на средних проектах с FSD не сталкивался.

          Вообще у меня было всего два случая общения с FSD, и оба — PaaS. Один ещё и разбитый по домену на несколько команд по 2-4 человека.

          Я подумал, что на средних проектах FSD может быть применим от того, что там сама сложность функционала не разрастается настолько, чтобы начинать путаться в FSD.

          Понимаю, что могу ошибаться.

          Расскажите подробнее, какие проблемы с FSD возникают на проектах среднего размера?


  1. Googlonator
    16.11.2025 17:42

    путаница случается между слоями Widgets и Features

    Если компонент можно назвать существительным - это виджет. Глагол(действие) - фича

    И однажды, когда вам понадобится переместить компонент с уровня widget на уровень features вы проклянете всё

    Тут проблема не в FSD а диких требованиях бизнеса если вам понадобилось перемещать целый widget в features. Ну либо в изначальной ошибке. И подобные изменение в любой методологии потребуют кучу изменений

    Не знаю, даже в крупных проектах FSD при правильной готовке не вызывает боли. Особенно в связке с effector & farfetched. Я так же предпочитаю немного нарушить методологию положив особенно важные сторы которые влияют на все приложение в shared слой.


    1. I0rrik Автор
      16.11.2025 17:42

      Если компонент можно назвать существительным - это виджет. Глагол(действие) - фича

      Ситуация: у нас есть функция создания пользователя в настройках в управлении пользователями и, допустим, в добавлении пользователя в проекту (допустим, у нас SaaS продукт).

      В списке настроек оно висит на собственном URL, а из проекта открывается в модалке. Такое ТЗ, так это увидели дизайнеры.

      В интерфейсе оно в зависимости от места применения называется "Создать пользователя".

      То есть, это глагол "создать", однако там много логики: нам нужно проверить пользователя на существование, запросить список проектов, в которые нужно добавить пользователя и определить ему роли (для этого нужно запросить ролевую модель), провалидировать форму, отправить данные на сохранение, обработать результат операции.

      Мы можем это назвать "Создание пользователя" (существительное) или "Создать пользователя" (глагол). Если переходим на английский, то это createUser либо userCreation. Тоже не особо помогло.

      Попробуем что-то попроще. Кнопка "Загрузить проекты" в интерфейсе. По сути, оно дёргает конкретный Get с параметрами и докидывает в интерфейс список проектов.

      Назвать можно "Загрузка проектов" или "Загрузить проекты".

      И так, по сути, со всем. Виджет, по сути, та же фича, только более комплексная. И вопрос в том, а где провести границу, что "вот до такой комплексности фича, а выше — виджет"?

      Не знаю, даже в крупных проектах FSD при правильной готовке не вызывает боли.

      Вероятно. Я не могу претендовать на истину в последней инстанции. Да и проектов на FSD видел только два. Однако я не могу придумать, как в крупном проекте приготовить FSD — не модифицируя его — так, чтобы это не было больно.

      Про эффектор мне сказать и вовсе нечего. Я с ним не работал т.к. не работаю в экосистеме React, а для vue подход не распространён.


    1. dsrk_dev
      16.11.2025 17:42

      Особенно в связке с effector & farfetched.

      К сожалению они приносят свои боли. Да это сильно лучше чемсм redux/mobx. Но эфектор Живёт в парадигме статических сторов, и для динамичски создаваемых сторов нужно писать костыли с фабриками.
      Последнее время инфраструктура особо не развивается. Роутер так и не добрался до релиза.
      Farfetched тоже не добрался до релиза, и тоже не развивается, pr с поддержкой zod 4 висит не счмёрженный несколько месяцев(а могли бы просто Standard Schema поддержать)


      1. dominus_augustus
        16.11.2025 17:42

        Effector лучше mobx, это в каком месте?


        1. dsrk_dev
          16.11.2025 17:42

          Холиварный вопрос, но я считаю mobx плохим стейт менеджером


    1. Unnemed112
      16.11.2025 17:42

      Тут проблема не в FSD а диких требованиях бизнеса если вам понадобилось перемещать целый widget в features. Ну либо в изначальной ошибке. И подобные изменение в любой методологии потребуют кучу изменений

      Как раз в этом и есть проблема FSD проектов находящихся на этапе перехода бизнеса от малого к среднему. FSD не адаптируем. Хорошая архитектура в буквальном смысле ждет изменений. фичи, слайсы, виджеты, как по мне это сущности приятнутые за уши и прибитые гвоздями к компонентам. Отсюда и проблема, что когда статус компонента меняется, становится непонятно что с ним делать. начинает или течь абстракция или лететь костыли. Слоистая архитектура в ствязке с атомик дизайном(как пример) в этом плане сильно гибче. Можете менять ядро системы, а ui и знать об этом не будет


  1. dsrk_dev
    16.11.2025 17:42

    Много лет строю достаточно большое и сложное приложение. Жили без fsd — было больно. Перешли на fsd — всё ещё больно. Как бы ты не старался, а сущности связанны друг с другом и влияют друг на друга. Да, по fsd не должно быть кроссимопртов между слоями, но в реальном мире это не возможно. А с кроссимпортами ещё и циклические зависимости приходять, которые стреляют неожиданно, и отлавливать их очень сложно.


    И есть подозрение что юз кейсы с доменами проблему тут не решат. Плюс менять архитектуру в существующем приложении очень больно и сложно. Не понятно как жить дальше


    1. I0rrik Автор
      16.11.2025 17:42

      Переписывать проект я не призываю :)

      Вообще, прежде чем избавляться от болей, стоит сперва определить, какие именно аспекты организации проекта причиняют боль. И только определив источники боли думать, как же от них избавиться.

      А на счёт FSD... С одной стороны мне хотелось бы взглянуть на крупный проект, в котором FSD работает как надо и помогает легче разрабатывать продукт. С другой стороны мне больше не очень хочется вообще трогать FSD :)


      1. karminski
        16.11.2025 17:42

        Было бы здорово увидеть реализацию вашего представления архитектуры, пример бы. В целом у вас здравые мысли, которые уже давно крутятся в голове, но никак не оформляются в релиз ;)


        1. I0rrik Автор
          16.11.2025 17:42

          Ну, кодовая база таких проектов зачастую под NDA, да и я сейчас без работы сижу. Благодаря чему у меня и появилось время на то, чтобы склепать эту статью :)

          А пет-проект такого масштаба, чтобы показать плюсы предложенной архитектуры, кажется чем-то очень долгим в исполнении.

          Разве что сделать mock-boilerplate для какой-то минимальной наглядности...


  1. jakobz
    16.11.2025 17:42

    Разделение на слои - это по-факту декомпозиция "для галочки". Ну типа всё красиво разложено по папочкам, в папочках - файлы с похожим содержимым. Но это не дает каких-то прям сильных плюшек. Каждое изменение затрагивает кучу файлов, которые непонятно кто еще использует. Тестировать - непонятно как, получаются дебильные тесты в стиле "если позвали API и fetch отдал объект X, то API отдал объект X".

    "Вертикальное" разделение - когда в каждой папочке-подсистеме какой-то понятный кусок функционала, с выставленным API - делать сложнее, но оно даёт плюшки. Можно какие-то вещи делать не выходя из этой папочки (если её API не меняется). На чанки приложение лучше разбивается. И - если вообще всё идеально - что-то можно выносить в библиотеки и использовать в нескольких приложениях. А может даже даже будет просто распилить приложение на два.

    Ошибка FSD - это чрезмерно настаивать каких видов нужно делать папочки-модули, и как они должны быть связаны. Ну типа различать features и widgets. Можно ведь делить как угодно, оставляя саму идею.

    По-факту, в реальных приложениях часто микс обоих подходов. Общие компоненты - почти всегда выделены как модули, правда не всегда следят чтобы API был нормальный. Какие-нибудь react hooks - сваливают в общую папку по принципу "это же хуки" - и те что общие, и те что нужны одной страничке.

    В общем, мысль в том, что думать надо чуть выше абстракциями. Ну, что каждая папочка - это модуль, от него должна быть какая-то польза (проще взять его, чем написать прям по месту), хороший API. И поменьше придумывать синтетические ограничения себе, в стиле "давайте заранее придумаем какие у нас папочки будут, и что будет в них".


    1. I0rrik Автор
      16.11.2025 17:42

      Я вижу центральной ошибкой FSD неправильную основу для абстракции.

      То есть FSD сосредотачивается на том, чтобы изолировать друг от друга бизнес-функции. В то время, как в первую очередь нужно изолировать бизнес-логику от внешних факторов: источников данных и представления. Это залог управляемости, масштабируемости и тестируемости бизнес-логики.

      FSD же наоборот связывает данные, бизнес-логику и представление в один комок, а потом размазывает это по нескольким слоям приложения. В результате получается нечто, чем довольно трудно управлять, и что весьма не просто масштабировать.

      Программа — это оцифровка бизнес-процессов, а сущности в бизнес-процессах всегда достаточно тесно переплетаются друг с другом.

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


      1. AlexViolin
        16.11.2025 17:42

        То есть FSD сосредотачивается на том, чтобы изолировать друг от друга бизнес-функции. В то время, как в первую очередь нужно изолировать бизнес-логику от внешних факторов: источников данных и представления. Это залог управляемости, масштабируемости и тестируемости бизнес-логики.

        Если FSD порождает столько проблем, которые описаны в статье и комментах, то возникает естественный вопрос, а зачем FSD вообще использовать. Берите многослойную архитектуру - она работает на всех масштабах от простейших приложений до огромных.


        1. I0rrik Автор
          16.11.2025 17:42

          Это нужно обдумать, честно говоря. Пока что сложным выглядит вопрос организации UI слоя и получения им информации от слоя бизнес-логики.

          Навскидку хорошим решением вижу объединением в слое UI подслоёв логики и получения данных в единый подслой module. Тогда получится

          • views — для всей композиции компонентов

          • model — для ui-логики и хранения ui состояния.

          В остальном укладывается хорошо.

          А, ещё непонятно, куда инфраструктуру укладывать...

          В общем, адаптация слоистой архитектуры под фронт — хороший вопрос на подумать, кмк.


        1. jakobz
          16.11.2025 17:42

          Типичный сценарий для современного IT:
          - берем общий термин или идею (FSD, или там Immutable State)
          - делаем как-получится реализацию (feature-sliced.design, или там Redux)
          - пиарим люто бешенно
          - у людей создаётся ассоциация FSD=feature-sliced.design, Immutable State = Redux
          - люди начинают считать изначальную идею - плохой, хотя плохая была - реализация

          FSD как идея, в том же реакте живёт прям от входа: компоненты лежат в папочке со своими хуками, CSS, и логикой. И отлично это работает. И по-инерции, народ пишет так не только компоненты. В общем, многие проекты на React (особенно без Redux) - они уже Feature-Sliced.

          Но нет - пришли люди с папочками, линтерами, и красивым веб-сайтом. Решили научить жить правильно. И вот - пошла справедливая контр-реакция. Но направленная теперь - против самой идеи.

          И вот, на полном серьезе людям предлагают переходить на Layered Architecture. Как на беке, да. Где, если что, вообще за все годы не научились делать большие проекты. Где с этими нашими слоями, всю дорогу не получалось ничего кроме лапши. И где единственный способ как-то себе запретить делать лапшу - прям физически разнести код по репозиториям. Это называется "микросервисы", и "микросервисы" переводится как "мы так и не научились разложить код по папочкам".

          Не надо тащить эту ошибку на фронт. Я вот сейчас наоборот пытаюсь эти идеи забирать в бек. Чем плоха монорепа, где у тебя те внутри сервисы, взаимодействующие друг с другом через понятные API? Разве это хуже, чем один гигантский DataLayer на сотни таблиц, и бизнес-логика - ходящая рандомно в любую часть БД?


          1. I0rrik Автор
            16.11.2025 17:42

            И да, и нет.

            Монолиты на бэке научились-таки готовить. Да, это трудно, однако микросервисы не стали панацеей. Там тоже достаточно своих проблем. И со сложностью инфраструктурной ахитектуры, и с синхронизациями баз, либо вообще со множественной миграцией микросервисов в одну базу. Для этого даже специальные инструменты придумывают, потому что сложна. В общем, оркестрация большого количества микросервисов та ещё головная боль, которая породила аж целую новую профессию девопса.

            И ещё большой вопрос, что хуже — с микросервисами сношаться или с монолитом.

            С фронтом, в целом, проблема похожая. Нам либо оркестрировать микрофронты (которые, так-то, тоже далеко не везде применимы), либо хреначить монолит.

            Да, можно напилить свой фронт под каждый эндпоинт, держать это в монорепе, но всё ещё остаётся вопрос с консистентностью этого зоопарка, с кэшированием, размерами бандла и т.д. и т.п...

            Для многих задач на фронте монолит всё ещё хороший, если и вовсе не единственный, вариант.

            А на счёт FSD... Ну, если фронт занимается только тем, что отправляет запросики в БД, то, да, особо запариваться с архитектурой правда не нужно. Можно напилить компонентики, пусть они запросы отправляют, данные получают и отображают. Логика плоская, всё будет работать чётко.

            Только вот не любой фронт ограничивается тем, что гоняет сетевые запросы.


      1. jakobz
        16.11.2025 17:42

        Всё очень сильно зависит от приложения. Я вот тоже пишу уже лет 18 бизнес-приложения, и какой-то прям отделяемой бизнес-логики на фронте - толком и не видел. Логика самого UI - да, бывает прям очень навороченной, всякие там WYSIWYG-билдеры, редактируемые диаграммы Ганта, и т.п. Но вот чтобы прям какую-то сложную функцию про бизнес, не привязанную к UI выделить - это скорее редкость. Это на беке всё обычно.

        Но я представляю бизнес-логики может быть много. У всех по-разному. У кого-то там API ходит в 50 микросервисов, постоянно бек просит переделать, и надо слой абстракции. Джависты любят нафигачить по REST своих GET/POST/PUT, и там даже для странички данные достать - уже на пол-страницы кода. А у кого-то - какой-нибудь один стабильный GraphQL, всё что хочешь достаётся одним запросом, и куда удобнее запросы эти класть рядом с компонентом.

        Из-за того, что у всех разное - рассуждения о том, какие именно должны быть папки прям в каждом приложении - скорее вообще губительны. Я видел классные, понятные, проекты, где рядом лежат папки с визуальными компонентами, хелперами про бизнес, и т.п - у любителей FSD волосы на жопе зашевелятся, но всё понятно и круто. И видел проекты где лид любит вот эти все папочки, и там пока что-то поймешь - через 10 слоёв надо пройти, менять что-то страшно, а разобраться что где - нереально.

        Работает понимание где код разбивать на модули, приёмы как это делать, здоровый перфекционизм, и трудолюбие чтобы постоянно что-то рефачить.


        1. I0rrik Автор
          16.11.2025 17:42

          куда удобнее запросы эти класть рядом с компонентом

          Когда запросы можно пересчитать по пальцам двух рук — да, удобно. Когда количество запросов разрастается и в разных местах приложения тебе для них нужно по-разному готовить данные, намного проще иметь унифицированный интерфейс и функции, которые будут заниматься адаптацией данных и брать на себя логику обработки пользовательского ввода, условий отображения и т.д...

          А ещё прикольно, когда у тебя запрос в тридцати компонентах используется и внезапно меняется контракт его использования. И сиди потом правь это всё говно в тридцати компонентах.

          Сколько сайд-эффектов так словишь по дереву?

          Нет, нужна инверсия ui от источников данных. Не всегда, но в сложных приложениях без этого повесишься через год. Максимум через два.


  1. alebard
    16.11.2025 17:42

    А что делать, если команда решила что FSD это про папочки? 1 слой - одна папка, 1 компонент. Хочешь декомпозировать большой компонент? создавай отдельный слой. Не подходит по смыслу? Создавай абстрактный слой с префиксом (subFeature, subWidget, etc...)

    Переубедить не получается


    1. I0rrik Автор
      16.11.2025 17:42

      Можно попробовать обратиться к архитектору, если таковой имеется и адекватен, чтобы он команде прописал сеанс групповой терапии.

      Можно обратиться к руководителю отдела и объяснить, чем чревата такая "архитектура". Лучше всего работает через "ожидаемая стоимость фичи будет n, тогда как при адекватной архитектуре можно снизить до n-m". Но будь готов в случае успеха (если получится защитить своё видение архитектуры) взять ответственность за провал (если команда будет срывать сроки, шишки посыпятся в первую очередь на тебя).

      Можно смириться и страдать. Это если рычагов влияния больше не осталось или ты не готов использовать те, что есть.


  1. luanan
    16.11.2025 17:42

    Добрый день! Очень понравилась статья, сам думал в эту сторону, после прочтения синей книги. Подскажите, как вы видите взаимодействие между сущностями? Вроде как в DDD для тесно связанных между собой сущностей используются агрегаторы.

    Возьмем такой кейс: есть канбан-доска с задачами. У задачи есть статусы, описание, комментарии, прикрепленные файлы. Для задачи может быть назначен исполнитель, автор и наблюдатель.

    Бизнесом закладываются определенные инварианты, например, что прикрепленный файл может удалить только автор задачи. Как бы подобная бизнес-логика выглядела в рамках вашей архитектуры?

    Меня тянет в сторону MVVM-паттерна, но как-будто бы наличие модели как класса с валидацией внутренних значений выглядит излишним, из-за того, что мы уже проверяем данные через ViewModel.


    1. I0rrik Автор
      16.11.2025 17:42

      Ну у меня тут от DDD только доменный слой.

      В кейса канбан-доска представляет из себя сложную сущность, которая состоит из:

      • ui

        • kanbanDeskPage — страница отображения доски. Состоит из глупых компонентов KanbanState, в которых рендерятся виджеты задач. Получает объект kanban доски, потом запрашивает список задач.

        • taskWiget — виджет задачи, принимает на вход объект задачи. Дальнейшую работу с задачей производит самостоятельно, чтобы не перегружать логикой страницу.

      • use-cases

        • useKanbanDesk обращается к репозиторию KanbanDeskRepository

          • getDesk(uuid) — в use-case слое. Содержит модель доски и функция дёргается из view. Модель доски содержит список задач по uuid

          • getDeskColumns(uuid) — use-case. Берёт из репозитория модель доски и вытаскивает из неё список столбцов.

        • useTasks

          • getGasks(ids: uuid[]) — возвращает массив объектов задач либо по PromiseAll, либо по PromiseAllSettled в зависимости от требований

          • useUpdateTask(uuid, task: Task) — метод управляет обновлением задачи. Если логика обновления исполнителя или наблюдателя сложная, можно вынести её в отдельную функцию useUpdateTaskExecutor или useUpdateTaskObserver.

      • repositories

        • KanbanDeskRepository — выполняет весь crud для kanban досок.

        • taskRepository — выполняет весь crud для задач. Занимается кэшированием в реактивном store, политиками оптимистичного и пессимистичного апдейта и т.п. Т.е., когда мы запрашиваем список задач доски, репозиторий этот список задач кэширует. Когда обновляем исполнителя, репозиторий отправляет запрос на обновление на сервер и обновляет кэш, когда получит ответ, чтобы сохранять консистентность данных и отображения.

      Прелесть этой структуры в том, что ui никак не органичен в использовании бизнес-логики, а бизнес-логика не имеет строгого ограничения на использование репозиториев. Если при удалении файла нам нужно проверить права пользователя, то мы в бизнес-функцию удаления файла можем передать идентификатор файла и идентификатор пользователя. Функция удаления запросит все данные, необходимые ей для работы и отправит запрос на выполнение в нужный репозиторий.

      При этом мы всё ещё не получили высокую связность, потому что ui ничего не знает о самих данных, только о контрактах взаимодействия, а бизнес-функции ничего не знают о том, для кого они выполняют работу.

      Конечно, некоторые изменения могут потребовать доработок, но пока что для меня это выглядит так, что доработки будут прозрачными, управляемыми и не чрезмерно объёмными.