惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 聂微东
IT之家
IT之家
月光博客
月光博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
爱范儿
爱范儿
Jina AI
Jina AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 司徒正美
Hacker News - Newest:
Hacker News - Newest: "LLM"
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
N
News and Events Feed by Topic
N
News and Events Feed by Topic
J
Java Code Geeks
Google Online Security Blog
Google Online Security Blog
The Cloudflare Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
www.infosecurity-magazine.com
www.infosecurity-magazine.com
NISL@THU
NISL@THU
L
Lohrmann on Cybersecurity
W
WeLiveSecurity
L
LINUX DO - 热门话题
S
Security @ Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
Webroot Blog
Webroot Blog
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
N
News | PayPal Newsroom
博客园 - 三生石上(FineUI控件)
S
Schneier on Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
V
V2EX
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
雷峰网
雷峰网
Scott Helme
Scott Helme
博客园_首页
P
Proofpoint News Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Cisco Talos Blog
Cisco Talos Blog
阮一峰的网络日志
阮一峰的网络日志
P
Privacy & Cybersecurity Law Blog
Project Zero
Project Zero
罗磊的独立博客
PCI Perspectives
PCI Perspectives
有赞技术团队
有赞技术团队
腾讯CDC
Cloudbric
Cloudbric
博客园 - 叶小钗
宝玉的分享
宝玉的分享

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как спроектировать приложение на годы вперед
Виталий · 2026-06-18 · via Все публикации подряд на Хабре

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

Лет двенадцать назад создание большого монолита было обычной практикой. Семь лет назад многие подсели на микросервисную архитектуру. Причем микросервисами часто называли все подряд: и сервисно-ориентированный подход (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. Но если границы ответственности проведены аккуратно, у команды остается шанс менять инструменты, не переписывая все приложение заново.