Мир технологий меняется быстро, и создать большое приложение, которое можно поддерживать несколько лет, становится непростой задачей.
Лет двенадцать назад создание большого монолита было обычной практикой. Семь лет назад многие подсели на микросервисную архитектуру. Причем микросервисами часто называли все подряд: и сервисно-ориентированный подход (SOA), и набор крупных сервисов, и распределенный монолит. Главное было быть в тренде.
Сейчас маятник снова качнулся. Микросервисы уже не выглядят универсальным ответом: слишком хорошо видна их цена в инфраструктуре, отладке, версионировании контрактов и сопровождении. Поэтому все чаще можно услышать про модульный монолит.
В этой статье я попробую набросать архитектурные и программные решения, которые позволяют создать и поддерживать веб-приложение в течение нескольких лет.
Ремарка
Я буду в основном опираться на личный опыт в .NET и TypeScript. Но сама идея не привязана к конкретному стеку.

Основная проблема
Возьмем за основу, что технологии и подходы меняются. Меняются версии .NET, TypeScript, React. Появляются и уходят моды на MobX, Redux, MediatR и другие инструменты.
Из этого не следует, что монолит - это всегда ошибка. Ошибка скорее в другом: строить большое приложение как единый ком, где любая новая бизнес-область цепляется за ядро, общую базу и общий подход к разработке.
Если система рассчитана на годы, хочется иметь возможность развивать ее части относительно независимо. Отсюда появляется желание выделять крупные этапы разработки и доменные области в самостоятельные блоки: где-то это будет модуль, где-то отдельный сервис, где-то фоновый процесс.
Как следствие, появляются две важные потребности.
Первая - понятная модель поставки и запуска. Если приложение начинает состоять из нескольких backend-сервисов, frontend-модулей, фоновых процессов и инфраструктурных компонентов, их уже неудобно держать как набор вручную настроенных процессов на сервере. Часто становятся нужны контейнеры, единый способ конфигурации, health checks, управление секретами, rolling update и возможность независимо масштабировать разные части системы. Для оркестрации такой среды обычно используют Kubernetes.
Вторая потребность - единая точка входа в систему. Когда frontend и backend перестают быть одним приложением, клиент не должен знать всю внутреннюю топологию: где живет профиль пользователя, где отчеты, где уведомления, а где фоновые операции. Один из вариантов для такой задачи - паттерн API Gateway. Он прячет внутреннее устройство системы, маршрутизирует запросы, держит общие технические правила входа и становится местом, через которое внешнему миру показывается цельное приложение, а не набор разрозненных сервисов.
Backend и инфраструктура
Исходя из описанного, к остальным частям приложения можно применить сервисно-ориентированный подход в широком смысле, то есть Service-Oriented Architecture (SOA). Kubernetes дает площадку для запуска таких частей, а API Gateway скрывает от клиента то, что происходит "под капотом". При этом реализация gateway может быть полностью ручной или основываться на готовой библиотеке вроде YARP.ReverseProxy. Это не принципиально. Важно другое: изменение внутренней реализации gateway не должно требовать изменений во внутренних сервисах, пока сохраняется внешний контракт.
Пока это выглядит как микросервисный подход, поэтому сразу оговорюсь. В ряде микросервисных реализаций API Gateway берет на себя не только проксирование и аутентификацию, но и часть авторизации: проверку ролей, политик доступа и разрешений на конкретные методы. На мой взгляд, для долгоживущего приложения это может быстро превратить gateway во второй слой бизнес-логики. Любое изменение API начинает требовать согласования не только контракта, но и правил доступа в центральной точке. Ошибка в этих правилах уже может нести реальные риски для бизнеса: например, если метод случайно окажется доступен не той роли.
Поэтому я бы оставил на API Gateway проксирование, маршрутизацию и аутентификацию, то есть проверку того, что пользователь в целом известен системе. А проверку доступа к конкретной функции лучше держать ближе к самой функции: в той backend-части, которая обслуживает конкретную страницу, виджет или действие интерфейса. Ниже я буду называть такую связку frontend и backend функциональным модулем. Тогда gateway остается инфраструктурной границей, а не превращается в центральный справочник всех бизнес-прав.
Frontend-модули
Теперь перейдем к frontend-части. Здесь с распространением новых архитектурных подходов все немного сложнее. В backend-части у нас давно есть привычные способы разделять систему на модули, сервисы и фоновые процессы. Во frontend похожая декомпозиция долго оставалась менее удобной: можно было делить код на пакеты, но собрать из независимых частей единое приложение во время работы было заметно сложнее.
С архитектурной точки зрения здесь появляется подход Micro Frontends: frontend разбивается на относительно независимые части, которые могут разрабатываться и поставляться отдельно. Одним из заметных технических механизмов для этого стала Module Federation. Она позволяет не просто вынести общий код в библиотеку, а подключать независимые frontend-проекты к общему shell-приложению. В терминах Module Federation такой проект обычно выступает как remote-контейнер и может отдавать наружу несколько компонентов: страницу, виджет или другой UI-блок. В рамках этой статьи именно такой внешний компонент я и буду называть frontend-частью функционального модуля.
За несколько лет экосистема вокруг Module Federation стала заметно более зрелой. Сейчас уже можно говорить не только о независимых проектах, но и о более сложных сценариях: общих зависимостях, динамической загрузке компонентов, SSR и отдельных сборках для разных частей системы. Да, на практике это часто привязывает нас к webpack или совместимым с ним решениям. Другие сборщики тоже развиваются, но для долгоживущей архитектуры важнее не гнаться за самым быстрым инструментом сборки, а выбрать решение, которое предсказуемо закрывает нужные сценарии. Скорость сборки можно частично компенсировать тем, что мы собираем не один большой frontend-монолит, а отдельные frontend-проекты или remote-контейнеры.
Но здесь есть важный нюанс - настройка локальной dev-среды. Модульная архитектура может быть хороша на схемах и в продакшене, но если разработчику больно запускать ее локально, команда быстро начнет воспринимать эту архитектуру как наказание. Формально каждый frontend-проект можно запустить как отдельное приложение. Но если для работы над одной задачей frontend-разработчику нужно поднять все remote-контейнеры сразу, он будет тратить силы не на задачу, а на борьбу с окружением.
Поэтому для такой архитектуры нужна отдельная локальная среда разработки. Например, docker-compose-проект, который поднимает shell и подтягивает remote-контейнеры с dev-площадки. А тот frontend-проект, в котором лежит frontend-часть текущего функционального модуля, подключается с локальной машины. Такая настройка требует дополнительной работы, которую не всегда видно в финальном продукте, но она сильно влияет на то, будет ли команда вообще хотеть пользоваться модульной архитектурой.
Можно пойти более простым путем и заставить каждого разработчика запускать все frontend-проекты локально. Для лида это действительно проще: меньше инфраструктуры, меньше специальных сценариев, меньше поддержки окружения. Но для команды такой путь быстро становится источником раздражения. А если архитектура раздражает людей каждый день, они начнут обходить ее при первой возможности.
Промежуточный итог
Мы структурно разделяем приложение на отдельные части и модули. Благодаря этому модуль может иметь свою frontend-часть, свою backend-часть и свой темп развития. Но для пользователя система при этом остается единым приложением.
Два слоя: модули и домены
В классическом модульном монолите систему часто делят по бизнес- или доменным областям. Модуль сотрудников не должен незаметно прорастать в модуль проектов, а модуль проектов - в модуль премий. У каждой области есть свои границы, свои данные и свои правила.
В этой статье я предлагаю взять из модульного монолита не саму ось разбиения, а дисциплину границ. Основной единицей здесь становится не доменная область, а функциональный модуль: страница, виджет, форма или другая часть интерфейса вместе с backend-слоем, который ее обслуживает.
То есть мы не строим "модуль сотрудников" или "модуль проектов" в классическом смысле модульного монолита. Мы строим, например, модуль карточки сотрудника, модуль согласования или виджет оценок - и не даем им прорастать друг в друга так же, как в модульном монолите не дают смешиваться доменным областям.
В такой схеме сразу появляются два слоя.
Первый - слой функциональных модулей. Функциональный модуль - это часть интерфейса вместе с backend-слоем, который ее обслуживает. Например: форма согласования, блок работы с оценками или страница карточки сотрудника.
Второй - слой доменов. Сотрудники, проекты, премии, документы и согласования никуда не исчезают, но они не становятся модулями интерфейсного приложения. В этой архитектуре доменный слой и функциональные модули - не одно и то же. На практике доменный слой можно представить как набор доменных сервисов: сервис сотрудников, сервис проектов, сервис премий и так далее. Они предоставляют модулям предметные данные, сложные выборки и общие операции над ними. Функциональный модуль отвечает за конкретную часть интерфейса и за то, как она собирается из frontend и backend частей.
Дальше я сначала опишу функциональный модуль как единицу интерфейса и backend-логики, а потом отдельно вернусь к доменному слою.
Функциональный модуль
Функциональный модуль - это архитектурная часть, которая включает в себя клиентскую реализацию и backend-слой, обслуживающий эту клиентскую часть. В таком подходе backend модуля знает не столько конкретную верстку, сколько состояние этой части интерфейса: что текущему пользователю доступно, какие действия разрешены, какие элементы нужно показать, а какие скрыть.
По сути, здесь используется идея Backend for Frontend. Backend функционального модуля не должен становиться владельцем данных домена. Его задача - принять запрос от пользователя, проверить доступ к этой части интерфейса, обратиться к нужным доменным сервисам и собрать ответ в удобном для frontend виде.
В этом смысле функциональный модуль работает и как Service Facade: закрывает от клиента внутреннюю схему сервисов и возвращает готовую модель отображения.
При этом функциональный модуль не обязан работать только с одним доменным сервисом. Страница или виджет часто собираются из данных разных областей: сотрудник, проект, премия, документы, права доступа. Поэтому backend-часть одного функционального модуля может обращаться сразу к нескольким доменным сервисам и собирать из их ответов одну модель для интерфейса.
Например, он может вернуть не просто список записей, а список записей вместе с доступными действиями для каждой из них: edit, delete, approve, sendToReview. Или вернуть признак, что пользователю доступно создание новой записи.
В результате frontend не обязан знать, какие роли, права или политики стоят за этим решением. Он получает уже готовое состояние интерфейса и отображает его. Правила можно менять на backend-стороне, а frontend при этом часто не придется пересобирать и публиковать заново.
Но здесь есть важная граница. Такие флаги и списки доступных действий нужны для отображения интерфейса, а не для защиты данных. Если backend сказал frontend не показывать кнопку удаления, это не заменяет проверку прав на endpoint удаления. Любая команда, которая меняет состояние системы, все равно должна повторно проверять права в backend-части функционального модуля, еще до вызова доменного сервиса. Иначе мы получим красивый интерфейс, но слабую безопасность.
Именно поэтому для такой схемы я бы не оставлял эту авторизацию на уровне API Gateway. Gateway не знает контекста конкретной страницы или виджета: какие действия сейчас доступны, какие данные уже загружены, в каком состоянии находится форма. Он может проверить, что пользователь в целом известен системе, но не должен становиться центром правил для каждого экрана.
Отдельная граница проходит между самими функциональными модулями. Один модуль не должен напрямую лезть во внутренние классы, таблицы, контроллеры или frontend-компоненты другого модуля. Backend-части модулей не должны ссылаться друг на друга напрямую. Допускается только вынос общей технической логики, контрактов или клиентов доменных сервисов в отдельные общие библиотеки, если они действительно не принадлежат конкретному модулю.
Из этого следует еще один практический момент: дублирование похожих endpoint-ов в разных модулях допустимо. Например, если нескольким модулям нужно получить фотографии сотрудников, каждый модуль может иметь свой backend-endpoint для этой задачи. Внутри он обратится к нужному доменному сервису или хранилищу, но не будет использовать контроллер или внутренний метод другого функционального модуля. На первый взгляд это выглядит как повторение кода, зато сохраняет независимость модулей.
С frontend-частью картина более-менее понятна: есть shell, есть Module Federation, есть remote-контейнеры, которые отдают наружу компоненты. При этом remote-контейнер не равен функциональному модулю. Один remote может экспонировать несколько страниц или виджетов, а модулем в нашей терминологии остается конкретная пара: frontend-часть страницы, виджета или другой интерфейсной области и backend-часть, которая ее обслуживает.
С backend возникает похожее различие. Backend-часть функционального модуля - это логическая часть архитектуры. А solution, проект, сервис или deployable-компонент - это уже способ упаковки одной или нескольких backend-частей. Их не стоит смешивать. В одном solution вполне может жить backend нескольких модулей, если они связаны общей предметной областью, общим этапом разработки или общей командой.
Если попытаться организовать backend в той же парадигме, в которой работает Module Federation, мы быстро упремся в зависимости. Во frontend remote-контейнеры имеют отдельные сборки и в некоторых сценариях могут жить с разными версиями зависимостей. В одном backend-процессе на .NET добиться такой же независимости намного сложнее. Если backend-части модулей начнут напрямую ссылаться друг на друга, со временем мы получим запутанный граф зависимостей, где любое изменение тянет за собой половину системы.
Можно ли держать backend-части всех модулей в одном большом проекте? Формально да. Но тогда есть риск превратить gateway или общий backend-хост в большой ком кода, который снова начнет знать слишком много. Мы как будто уйдем от монолита, но потом соберем его обратно в другом месте.
Поэтому backend-части модулей можно группировать, но делать это нужно осознанно. Иногда их удобно собирать в один solution по предметной области: например, все модули, связанные с оценками на проекте. Иногда - по крупному этапу развития продукта, когда очередной большой блок доработок заказчика оформляется как группа модулей. Важно, что группа модулей - это не сам модуль, а способ собрать и сопровождать несколько модулей вместе.
Такое разделение позволяет не застревать навсегда на одной версии платформы и не ломать уже работающие части системы при развитии новых. Но за эту свободу приходится платить дисциплиной. Backend-части модулей не должны ссылаться друг на друга напрямую, а у frontend-части модуля должна быть понятная backend-часть, которая обслуживает именно эту часть интерфейса.
Слой доменов
Теперь перейдем ко второму слою. Доменные сервисы не являются публичным API для браузера. К ним обращаются backend-части функциональных модулей, когда им нужны предметные данные, сложные выборки или общие операции над ними.
На уровне доменов перестает использоваться пользовательская авторизация в привычном UI-смысле. Доменный сервис не должен знать, показывать ли пользователю кнопку approve или пункт меню delete. Этим занимается функциональный модуль. Доменный сервис может проверять доверенность вызывающего модуля через service credentials, но эти credentials не завязаны на конкретного пользователя.
По своей роли доменный сервис здесь частично напоминает паттерн Repository или предметный сервис доступа к данным, но не сводится только к ним. Он умеет получать и сохранять данные своей области, строить сложные запросы и выборки, агрегировать информацию, выполнять общие расчеты и базовые проверки. Например, посчитать НДС, проверить корректность входных данных или не дать записать явно неконсистентное состояние. Это помогает не размазывать одинаковые запросы, расчеты и проверки по функциональным модулям.
Функциональный модуль отвечает за пользовательский доступ и форму ответа для интерфейса. Доменный сервис отвечает за предметные данные, запросы, выборки и общие операции своей области.
Здесь мы уже идем по более классической схеме сервисов. Сервисы делятся по зонам ответственности, то есть по доменам: сотрудник, проект, премия и так далее. Взаимодействие таких сервисов можно строить через шину событий, то есть с использованием элементов event-driven architecture.
Идея простая: если доменный сервис изменил данные, он публикует событие. Например, сотрудник был создан, проект изменил статус, премия была пересчитана. Это событие не является командой другому сервису "сделай вот это". Скорее это факт, который уже произошел в домене. Остальные части системы могут на него отреагировать, если им это нужно.
В реальной реализации рядом с этим может появиться паттерн Transactional Outbox. Но это уже не про саму архитектурную границу, а про надежность доставки. Если доменный сервис уже сохраняет данные и должен гарантированно опубликовать событие, он может зафиксировать изменение данных и запись события вместе, а отдельный процесс потом отправит событие в шину.
С обратной стороны иногда используют Inbox: потребитель сохраняет факт обработки сообщения, чтобы безопасно переживать повторы. Но это нужно не каждому модулю. Если функциональный модуль не хранит собственную проекцию и просто реагирует на событие, ему не обязательно заводить базу данных только ради участия в событийной схеме.
Событие может обработать другой доменный сервис. Например, сервис проектов может сохранить у себя минимальную проекцию сотрудника, если она нужна для быстрых выборок по проектам. Событие может обработать и функциональный модуль: обновить свою локальную модель отображения, пересобрать список, инвалидировать кэш или подготовить данные для конкретного виджета.
Здесь появляется важный момент, часто встречающийся в распределенных системах: дублирование данных допустимо, если мы понимаем, зачем оно нужно и кто остается источником истины. Если сервис премий хранит у себя имя сотрудника для отчета, это не значит, что он стал владельцем сотрудника. Владельцем остается доменный сервис сотрудников, а остальные хранят копии, проекции или вычисленные представления.
Такие копии могут обновляться асинхронно. Значит, система должна спокойно относиться к eventual consistency: данные в разных местах могут сходиться не мгновенно. Для некоторых экранов это нормально, для других придется делать прямой запрос к доменному сервису или явно показывать пользователю состояние обработки.
Еще один полезный прием - возможность восстановить производные данные. Если модуль или сервис хранит локальную проекцию, он должен уметь пересобрать ее: запросить актуальное состояние у доменного сервиса, перечитать события или запустить отдельный процесс синхронизации. Тогда дублирование данных становится не хаотичным копированием, а осознанной частью архитектуры.
Общая схема

Общий поток запроса
Если собрать все вместе, получается такая схема.
Клиентское приложение обращается не к доменным сервисам напрямую, а к API Gateway. Gateway проверяет, что пользователь в целом известен системе, и проксирует запрос в backend-часть нужного функционального модуля.
Backend-часть функционального модуля находится между frontend и доменным слоем. Она понимает контекст страницы или виджета, проверяет пользовательский доступ, собирает данные для интерфейса и при необходимости обращается к одному или нескольким доменным сервисам.
Функциональный модуль обращается к доменному сервису с конкретным запросом или командой: получить данные, построить выборку, сохранить изменения, выполнить расчет или базовую проверку. Доменный сервис выполняет эту операцию в рамках своей предметной области. Если в результате данные изменились, именно доменный сервис публикует событие в шину.
Событие могут обработать другие доменные сервисы, если им нужно обновить свои проекции или синхронизировать производные данные. Это же событие могут обработать и функциональные модули: например, обновить локальную модель отображения, инвалидировать кэш или отправить уведомление клиенту.
На практике я бы не делал публикацию событий обязанностью функциональных модулей. Возможно, где-то такой кейс найдется, но базовое правило проще: событие о доменном изменении публикует тот доменный сервис, который это изменение сделал. Функциональный модуль может инициировать операцию и может читать события, но не должен становиться источником доменных событий.
Отсюда появляется еще один полезный сценарий. Функциональный модуль может подписаться на событие доменного слоя, обработать его и через SignalR отправить сообщение уже конкретным клиентам. Например, доменный сервис пересчитал премию, опубликовал событие, модуль виджета уведомлений его обработал и отправил пользователю обновление интерфейса. При этом доменный сервис ничего не знает ни про SignalR, ни про открытые вкладки браузера, ни про то, какой виджет сейчас отображает эти данные.
Взаимодействие на frontend
На frontend тоже может возникнуть потребность во взаимодействии между разными частями приложения. Например, один виджет изменил состояние, а другой должен обновиться. Или общий shell получил событие, которое нужно передать нескольким frontend-модулям.
Здесь можно использовать тот же общий принцип: не давать одному модулю напрямую лезть во внутреннее состояние другого. Вместо этого можно ввести клиентскую шину событий. В терминах паттернов это Event Bus и Publish/Subscribe внутри frontend-приложения. Например, такую схему можно реализовать через библиотеку js-event-bus.
Такой event bus не должен превращаться в глобальную помойку для всего подряд. Лучше считать его механизмом для межмодульных событий: один модуль сообщает факт, а другие модули, если им это нужно, реагируют. При этом событие должно быть достаточно нейтральным: не "поменяй мне вот этот store", а "изменился выбранный проект", "обновился список уведомлений", "пользователь сменил контекст".
Особенно полезно это становится при работе с сокетами. Если каждому функциональному модулю открыть свой SignalR connection, приложение быстро начнет плодить лишние подключения, усложнит авторизацию, переподключение и диагностику. Поэтому можно сделать отдельный frontend-модуль событий. У него может вообще не быть интерфейса. Его задача - держать одно или несколько socket-подключений, принимать клиентские события с backend и публиковать их уже во внутреннюю frontend-шину.
Да, такой модуль немного нарушает идею полной независимости частей приложения. Остальные модули начинают зависеть от набора клиентских событий. Но это осознанный компромисс: мы централизуем работу с SignalR, уменьшаем количество подключений и не размазываем логику переподключения по всем виджетам и страницам.
В итоге получается похожая схема, только уже внутри браузера: backend прислал событие по SignalR, frontend-модуль событий его принял, опубликовал во внутренний event bus, а заинтересованные виджеты или страницы обновили свое состояние.
Заключение
В заключение хочется сказать, что в этой схеме нет какой-то магии или нового серебряного паттерна. Почти все элементы давно известны: API Gateway, Backend for Frontend, Micro Frontends, SOA, event-driven architecture. Вопрос не в том, чтобы назвать их все, а в том, чтобы правильно провести границы между частями системы.
Если эти границы размываются, любое приложение со временем начинает превращаться в большой ком кода. Неважно, называется оно монолитом, микросервисами или модульной архитектурой. Если gateway знает слишком много, модули лезут друг в друга, а доменный слой превращается в свалку общей логики, название подхода уже не спасет.
Поэтому главный вывод для меня такой: долгоживущее приложение строится не вокруг конкретной технологии, а вокруг дисциплины разделения ответственности. Функциональный модуль должен отвечать за свою часть интерфейса и ее backend-слой. Доменный сервис должен отвечать за данные, запросы, выборки и общие операции своей предметной области. API Gateway должен оставаться входной инфраструктурной границей, а не центром всех бизнес-правил.
За такую архитектуру приходится платить. Нужна нормальная локальная среда разработки, понятные контракты, аккуратная работа с событиями и готовность иногда принять небольшое дублирование ради меньшей связанности. Но если цель - приложение, которое можно развивать несколько лет, эта цена выглядит разумнее, чем постоянная борьба с системой, где любое изменение задевает половину проекта.
Возможно, через несколько лет часть технологий снова изменится. Появятся другие сборщики, другие фреймворки, другие способы доставки frontend и backend. Но если границы ответственности проведены аккуратно, у команды остается шанс менять инструменты, не переписывая все приложение заново.
Комментарии (28)

Dhwtj
17.06.2026 18:50Из одного пост мортем: как умирают проекты
Проект жил 11 лет пока не умер
Топ-5 худших решений по вкладу в случайную сложность
Массовое расщепление 2018 г. — перенос кода в десятки независимых репозиториев без чётких bounded context. Стало корнем проблемы сборки, версионирования и навигации. Привлечение внешних подрядчиков.
Замена ProjectReference на внутренний NuGet. Локально — «чистое разделение версий», глобально — dependency hell и невозможность сборки без корпоративного feed’а. Soap вместо монолита.
Создание xxx.Api.WebService как форка xxx.Api вместо рефакторинга. Старый в итоге не удалён. Параллельные API — классический пример локально обоснованного, но глобально разрушительного решения.
Xxx.ReportServiceNet как второй сервис отчётности (2023) вместо постепенной замены xxx.ReportService. Ещё один живой дубль.
Миграция на .NET 5/6/9 без вывода .NET Framework/WCF. Технологическое «слоёное пирожное»: каждое новое поколение добавлялось, не убирая предыдущее. В итоге ~36% проектов остаются на .NET Framework и блокируют Linux-перенос.
Итоговый вердикт по исходной системе Привнесённая сложность исходной системы многократно превышает сложность задачи. Это не одна архитектурная ошибка, а результат системного накопления: 90% кода — не доменная сложность, а инфраструктура, legacy, дублирование и организационный шаблон.
Только 4 группы дублирующих сервисов дают 1 972 файла — на 32% больше, чем вся доменная логика.
245 проектов (36%) — унаследованный .NET Framework.
853 внутренних NuGet-ссылки вместо прямых ссылок на исходники.
Миграция legacy в 12–17 раз дороже, чем переписывание с нуля.
Разница между миграцией legacy и переписыванием с нуля — примерно 10–20 раз по трудоёмкости. Это подтверждает, что основная сложность исходной системы — не доменная (essential), а случайная (accidental), накопленная решениями, которые локально казались разумными, но глобально не масштабировались
Dual persistence: SQL Server + MongoDB (мешает аналитике связанных данных - join уже не сделать)
Причины выбора двух баз:
Система начиналась на SQL Server.
Объёмы данных выросли, и MongoDB добавили как быстрое хранилище для временных рядов.
Страх перед SQL Server на high-write нагрузке.
Мода на NoSQL в 2014–2018.
Короче, ад
Финальный диагноз: техническое банкротство
Система запуталась в собственной архитектуре и не смогла выбраться. Цепочка:
Мода на микросервисы (2017–2019)
↓Разделение на 137 репозиториев (2018)
↓Внутренний NuGet для “независимости” (2018–2020)
↓Dependency hell + рассинхронизация версий
↓Локально оптимальные решения становятся глобально сложными
↓Миграция на .NET 5/6/9 (2020–2023)
↓Невозможность выключить legacy:
это ядро бизнеса
нет автотестов
-
853 NuGet-ссылки, никто не знает все зависимости
↓
Система поддерживает 5 поколений .NET

MonkAlex
17.06.2026 18:50По тексту складывается впечатление, что проблема в "неудалении" старых сервисов\кода. Но это ресурсы разработки-тестирования как минимум, которые придётся отвлечь от фичей -> отвлекаем ресурсы, уменьшаем продуктивность, можем получить более ранний коллапс, нет?
Просто я вижу похожие схемы и магии не случилось нигде, всем приходится жить в таком формате. Монолит перестает справляться с растущей нагрузкой, распиливаем монолит на микросервисы, процесс бесконечный - надо и фичи продолжать делать и нагрузку выносить и ещё много всего.

Dhwtj
17.06.2026 18:50Вид с вертолёта:
Не подумали, что придется переходить на линукс
Пытались в независимость команд, но крайне неудачно: тут и ад пакетов и микросервисы (вернее, soap на WCF, но без ESB), хотя современный гит умеет дать доступ к части солюшна
Метания и незаконченность перехода (не отказались от старых модулей потому что не смогли доказать надёжность миграции), так куски от обоих частей и живут
Микросервисы были бы чуть лучше: WCF гадил в домене, который зависит теперь от структуры БД. Но я бы на монолите всё сделал. Вернее, на монорепо с небольшим числом сервисов.

Arsch_des_Prasidenten
17.06.2026 18:50Это не про авито ли?

Dhwtj
17.06.2026 18:50Нет. Корпоративная система, не публичная.
Вот сейчас все недовольны что прибито гвоздями к винде. Но сделать уже ничего нельзя.

Dreams_and_magic
17.06.2026 18:50Почему нельзя?

Dhwtj
17.06.2026 18:50Ну блин
У попа была собака
Он её любил
Она съела кусок мяса
Он её убил
И в землю закопал
И надпись написал
Goto 1

Alex_RF
17.06.2026 18:50На самом деле как не проектируй, все равно если 1) через пару лет прийдет кто-то кто скажет - не спроектировано не так - я переделаю 2) разработчик скажет - да все равно - а я вот так могу 3) менеджер скажет - что вы тут в облаках летаете с архитектурой надо быстрее.... все просто привратится в BigPieceOfMud и умрет.

RussianDragon Автор
17.06.2026 18:50Ну тут простые ответы :)
Переделать всё может только тимлид и то после согласования с архитектором.
Для отслеживания того, что пишет "рядовой" разработчик и есть пулреквесты. Модули разделены по проектам, то как следствие отслеживать нарушение концепциий можно без особых усилий.
Ну тут нужно согласование и одобрение скорее не менеджера, а архитектора и тимлида. Именно они решают, как проект удобней будет поддерживать.

akardapolov
17.06.2026 18:50За несколько лет инструменты вокруг Module Federation стали заметно зрелее.
Не звучит, похоже на буквальный перевод с английского с mature.
Вот так лучше КМВ: За несколько лет экосистема вокруг Module Federation стала более зрелой.
Но здесь есть важный нюанс - настройка локальной dev-среды.
…
Поэтому для такой архитектуры нужна отдельная локальная среда разработки.
Только так строю свою работу. В текущих реалиях когда граница между фронтом и бэком размывается и нужна очень быстрая скорость обратной связи (feedback loop), зазор между намерением что-то реализовать и получением результата надо уменьшать, насколько это позволяют технологии и наши когнитивные ограничения (без Human-in-the-Loop (HITL) пока никуда).

RussianDragon Автор
17.06.2026 18:50За несколько лет экосистема вокруг Module Federation стала более зрелой.
Поправил, для удобства. Но сути текста это не меняет.
Во втором блоке я имел виду, что модули локально можно запускать по разному. Можно пустить всё на самотек, и люди сами будут запускать большую часть фронта руками. Даже те модули, которые сейчас не нужно править, но нужны для финального отображения. Можно добавить файл со скриптом, для автозапуска таких частей, можно сделать отдельный преднастроенный контейнер и запускать его через docker-compose файл. Просто я это проговариваю, т.к. это не размышления из вакуума, а из опыта создания проектов.

cmyser
17.06.2026 18:50Как то переусложненно получается

RussianDragon Автор
17.06.2026 18:50Как показала практика, в ретроспективе, получаем стабильную структуру и довольно простое восприятие проекта на всё время.
Монолит через года требует обновление фрейморков, а это огромные затраты. Микросервисы накапливают множество методов. И сложнее следить за теми методами которые могли выйти из эксплуатации, но их продолжают поддерживать.
В общем и целом такой подход позволяет удержать единый уровень "сложности" и подход к проекту.
Более того т.к. страницы и виджеты развязаны и не пересекаются, таки эм образом можно легко модифицированный части проекта без опасения сломать в других местах. А как следствие спрект легче поддаётся модификациям.

cmyser
17.06.2026 18:50Много человек-фактора остаётся
Тот же bff вообще странная тема, сначала спроектируем плохо, а потом сделаем ещё один сервис что бы отдавал данные как надо, даже звучит плохо
Про федерации на фронте слышу от знакомых только плохое, там либо 1 фрейморк, либо никак
Нам нужны программные гарантии которые будут снижать сложность систем
Для фронта это $mol framework, который может быть и федерацией и монолитом и микрофронтами. Он основан на технических анализах, за счёт чего кратно снижаются сложности как разработки так и архитектуры
Для Бэка нам нужен новый протокол, который бы убрал потребность в bff. Называется HARP ( human API rest protocol ) вот он ещё только на бумаге есть

RussianDragon Автор
17.06.2026 18:50Много человек-фактора остаётся
Да, полностью человеческий фактор убрать нельзя. Но его можно сильно уменьшить за счёт структуры модулей, явных соглашений и конфигурации.
На бэке можно достаточно явно отследить, если один функциональный модуль начинает ссылаться не туда или обращаться к чужой зоне ответственности напрямую. На фронте тоже можно изначально задать соглашения в конфигурации: например, чтобы часть URL/path указывала на конкретный функциональный модуль. Тогда такие нарушения сложнее сделать случайно, и их проще увидеть на review.
Тот же bff вообще странная тема, сначала спроектируем плохо, а потом сделаем ещё один сервис что бы отдавал данные как надо, даже звучит плохо
Я уже подумываю убрать термин BFF из статьи, потому что на него многие обратили внимание и, похоже, он уводит обсуждение немного в сторону.
В моём случае я вкладывал в него не смысл “сделали плохой API, а потом прикрыли его ещё одним сервисом”, а скорее смысл адаптационного слоя под конкретный клиента (виджета, страницы). Например, такой слой может:
агрегировать данные: сходить в User Service, Orders Service, Payment Service и вернуть frontend уже готовый объект;
преобразовывать формат ответа: внутренние сервисы могут отдавать сложные DTO, а наружу возвращается удобный ViewModel-формат;
скрывать внутреннюю архитектуру: frontend не знает, сколько микросервисов внутри и как они устроены;
оптимизировать API под конкретный клиент — web, mobile или фронтовую часть функционального модуля.
То есть идея не в том, что мы сначала плохо спроектировали систему, а потом пытаемся это исправить. Идея в том, что логика, связанная с конкретным пользовательским сценарием, сосредоточена в одном месте — в рамках функционального модуля.
У нас были случаи, когда web-фронт и мобильное приложение смотрели на один и тот же функциональный модуль. При этом логика прав менялась только на бэке, а web и mobile автоматически подстраивались под неё через возвращаемое состояние/доступные действия.
Про федерации на фронте слышу от знакомых только плохое, там либо 1 фрейморк, либо никак
С Module Federation у нас был опыт, где на одном проекте приходилось совмещать React, Vue и Angular. Разные подрядчики делали компоненты на разных технологиях, и всё это нужно было отображать в рамках одной страницы. Как так получилось — отдельная история и не совсем была связана с нами. Но технически через Module Federation можно подружить разные технологии. При этом я согласен, что это не значит, что так нужно делать без необходимости. Если есть возможность держаться одного фреймворка и единых стандартов, это обычно проще.
Для фронта это $mol framework, который может быть и федерацией и монолитом и микрофронтами.
Про $mol framework честно пока не могу уверенно сказать. Нужно отдельно изучать и пробовать.
Называется HARP ( human API rest protocol ) вот он ещё только на бумаге есть
Я бы не сказал, что то, что мы делали, можно назвать HARP. Но поверхностно некоторые идеи действительно пересекаются: например, стремление описывать API так, чтобы клиент мог получить нужную структуру данных без дополнительного слоя адаптации.

cmyser
17.06.2026 18:50Спасибо за такой развернутый ответ
Согласен про соглашения, но я бы их старался усилить программыми методами, ci, гит хуки обязательно(чем раньше находим проблему тем лучше)
Но мало какой инструмент помогает программно поддерживать соглашения, линтеры часто не могут этого обеспечить
A про харп подробнее вот тут https://habr.com/ru/articles/680376/
Он как раз может добавить программные соглашения, и тогда не понадобится слой адаптации данных

ironvd
17.06.2026 18:50Где хранится логика обеспечивающая расчет прав доступа, в каком слое, service credential? Правильно ли я понимаю, что она дублируется как в самом модуле, так и в самом gateway или все функции обращаются к service credential? Например, нужно скрыть кнопку создания сущности определенной роли, отдать какой-то флаг фронту и при этом, при выполнении запроса напрямую, так же запретить операцию для этой роли.
По поводу шины событий для фронтенда, как вы решали обновление для нескольких открытых вкладок, через BroadcastChannel? Были ли еще какие-то проблемы, например с обновлением jwt токена (если используется, конечно)?

RussianDragon Автор
17.06.2026 18:50Пользователи у нас хранились во внешней системе — например, Active Directory / OAuth2. Также есть вариант, когда логин/пароль остаются на уровне базы API Gateway, но это касается именно аутентификации.
Список доступных ролей и разрешений пользователя хранится в доменном сервисе прав/разрешений. На практике система прав чуть сложнее, чем просто роли, но суть в том, что источник прав у нас один. Логика расчёта доступов не размазывается между Gateway и разными модулями.
По поводу примера с кнопкой: у нас есть два метода. Первый метод возвращает состояние страницы для фронтенда: какие действия доступны и какие кнопки показывать. Второй метод непосредственно создаёт сущность. Оба метода находятся в одном функциональном модуле и используют одни и те же правила проверки прав.
То есть фронтенд может скрыть кнопку на основании флага, полученного с сервера, но это не является единственным механизмом защиты. Если пользователь выполнит запрос напрямую, сервер всё равно проверит права в методе создания сущности и запретит операцию, если прав недостаточно.
---
В статье описана шина событий на уровне одной вкладки. Более сложные варианты, например BroadcastChannel, не было необходимости применять, хотя в целом мы их иногда рассматривали.
JWT у нас хранится в cookies и обновляется по мере выполнения запросов. Отдельных проблем с синхронизацией токена между вкладками из-за этого подхода не возникало.

ironvd
17.06.2026 18:50Значит единый источник истины для прав доступа - это здраво. Спасибо за статью, полезная информация.

CHEM_Eugene
17.06.2026 18:50Спасибо большое за статью, очень интересно!
Я не очень понял где все таки проходят границы микросервисов в вашем случае. Находится ли все в модульном монолите (как одна единица деплоймента) или каждый домен это отдельный сервис?
APIGW и BFF деплоятся все вместе как один сервис или раздельно?
RussianDragon Автор
17.06.2026 18:50У нас есть два слоя: функциональные модули и доменные сервисы.
Доменные сервисы — это отдельные сервисы со своими границами ответственности. А вот со словом “микросервис” я бы был аккуратнее, потому что оно сразу накладывает дополнительные ожидания и требования, хотя они и близки к ним.
Функциональные модули — это логические модули, обычно на уровне отдельных csproj. В идеальном мире каждый такой функциональный модуль можно было бы вынести в отдельный solution. Но на практике так не получится - не хватает ресурсов: появляется слишком много репозиториев, пайплайнов, деплоев, инфраструктурной обвязки и операционной сложности.
Поэтому часть функциональных модулей объединяется в группы и разворачивается вместе — фактически как модульный монолит. При этом внутри остаются логические границы между модулями.
Как именно бить функциональные модули на группы — отдельный вопрос. Можно группировать по командам, по доменной зоне, по крупным этапам развития продукта или даже по техническим причинам, например при переходе на новую версию .NET.
API Gateway у нас — отдельный сервис.

FlyGst
17.06.2026 18:50Написано для идеального мира? ;) Про текучку кадров не забывайте.

RussianDragon Автор
17.06.2026 18:50Как раз наоборот.
Т.к. система малосвязанная. В старый код (проекты) лезть уже не нужно. Можно спокойно пилить новые страницы новой командой.
Если потребуется правки, то распределённость логики позволит на функциональных модулях вносит изменения без опасения сломать соседние страницы (части системы).
Доменные сервисы тоже не обладают сложной логикой и легче, чем микросервисы для восприятия.
Dhwtj
Принцип минимального знания
Low coupling high cohesion
И так далее
А в .net framework ещё и было принято засирать домен знаниями о структуре БД. И все (включая MS) говорили что это нормально. Зато вендор лок ого-го, хрен перейти