Обновить

Как спроектировать web-приложение на годы вперед

Уровень сложностиСредний
Время на прочтение15 мин
Охват и читатели13K
Всего голосов 5: ↑5 и ↓0+7
Комментарии28

Комментарии 28

Принцип минимального знания

Low coupling high cohesion

И так далее

А в .net framework ещё и было принято засирать домен знаниями о структуре БД. И все (включая MS) говорили что это нормально. Зато вендор лок ого-го, хрен перейти

НЛО прилетело и опубликовало эту надпись здесь

Из одного пост мортем: как умирают проекты

Проект жил 11 лет пока не умер

Топ-5 худших решений по вкладу в случайную сложность

  1. Массовое расщепление 2018 г. — перенос кода в десятки независимых репозиториев без чётких bounded context. Стало корнем проблемы сборки, версионирования и навигации. Привлечение внешних подрядчиков.

  2. Замена ProjectReference на внутренний NuGet. Локально — «чистое разделение версий», глобально — dependency hell и невозможность сборки без корпоративного feed’а. Soap вместо монолита.

  3. Создание xxx.Api.WebService как форка xxx.Api вместо рефакторинга. Старый в итоге не удалён. Параллельные API — классический пример локально обоснованного, но глобально разрушительного решения.

  4. Xxx.ReportServiceNet как второй сервис отчётности (2023) вместо постепенной замены xxx.ReportService. Ещё один живой дубль.

  5. Миграция на .NET 5/6/9 без вывода .NET Framework/WCF. Технологическое «слоёное пирожное»: каждое новое поколение добавлялось, не убирая предыдущее. В итоге ~36% проектов остаются на .NET Framework и блокируют Linux-перенос.

  6. Итоговый вердикт по исходной системе Привнесённая сложность исходной системы многократно превышает сложность задачи. Это не одна архитектурная ошибка, а результат системного накопления: 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 уже не сделать)

Причины выбора двух баз:

  1. Система начиналась на SQL Server.

  2. Объёмы данных выросли, и MongoDB добавили как быстрое хранилище для временных рядов.

  3. Страх перед SQL Server на high-write нагрузке.

  4. Мода на NoSQL в 2014–2018.

Короче, ад

Финальный диагноз: техническое банкротство

Система запуталась в собственной архитектуре и не смогла выбраться. Цепочка:

Мода на микросервисы (2017–2019)

Разделение на 137 репозиториев (2018)

Внутренний NuGet для “независимости” (2018–2020)

Dependency hell + рассинхронизация версий

Локально оптимальные решения становятся глобально сложными

Миграция на .NET 5/6/9 (2020–2023)

Невозможность выключить legacy:

  • это ядро бизнеса

  • нет автотестов

  • 853 NuGet-ссылки, никто не знает все зависимости

Система поддерживает 5 поколений .NET

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

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

Вид с вертолёта:

  • Не подумали, что придется переходить на линукс

  • Пытались в независимость команд, но крайне неудачно: тут и ад пакетов и микросервисы (вернее, soap на WCF, но без ESB), хотя современный гит умеет дать доступ к части солюшна

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

Микросервисы были бы чуть лучше: WCF гадил в домене, который зависит теперь от структуры БД. Но я бы на монолите всё сделал. Вернее, на монорепо с небольшим числом сервисов.

Это не про авито ли?

Нет. Корпоративная система, не публичная.

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

Почему нельзя?

Ну блин

У попа была собака

Он её любил

Она съела кусок мяса

Он её убил

И в землю закопал

И надпись написал

Goto 1

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

Ну тут простые ответы :)

  1. Переделать всё может только тимлид и то после согласования с архитектором.

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

  3. Ну тут нужно согласование и одобрение скорее не менеджера, а архитектора и тимлида. Именно они решают, как проект удобней будет поддерживать.

За несколько лет инструменты вокруг Module Federation стали заметно зрелее.

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

Вот так лучше КМВ: За несколько лет экосистема вокруг Module Federation стала более зрелой.

Но здесь есть важный нюанс - настройка локальной dev-среды.

Поэтому для такой архитектуры нужна отдельная локальная среда разработки.

Только так строю свою работу. В текущих реалиях когда граница между фронтом и бэком размывается и нужна очень быстрая скорость обратной связи (feedback loop), зазор между намерением что-то реализовать и получением результата надо уменьшать, насколько это позволяют технологии и наши когнитивные ограничения (без Human-in-the-Loop (HITL) пока никуда).

За несколько лет экосистема вокруг Module Federation стала более зрелой.

Поправил, для удобства. Но сути текста это не меняет.

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

Как то переусложненно получается

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

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

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

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

Много человек-фактора остаётся

Тот же bff вообще странная тема, сначала спроектируем плохо, а потом сделаем ещё один сервис что бы отдавал данные как надо, даже звучит плохо

Про федерации на фронте слышу от знакомых только плохое, там либо 1 фрейморк, либо никак

Нам нужны программные гарантии которые будут снижать сложность систем

Для фронта это $mol framework, который может быть и федерацией и монолитом и микрофронтами. Он основан на технических анализах, за счёт чего кратно снижаются сложности как разработки так и архитектуры

Для Бэка нам нужен новый протокол, который бы убрал потребность в bff. Называется HARP ( human API rest protocol ) вот он ещё только на бумаге есть

Много человек-фактора остаётся

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

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

Спасибо за такой развернутый ответ

Согласен про соглашения, но я бы их старался усилить программыми методами, ci, гит хуки обязательно(чем раньше находим проблему тем лучше)

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

A про харп подробнее вот тут https://habr.com/ru/articles/680376/

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

Где хранится логика обеспечивающая расчет прав доступа, в каком слое, service credential? Правильно ли я понимаю, что она дублируется как в самом модуле, так и в самом gateway или все функции обращаются к service credential? Например, нужно скрыть кнопку создания сущности определенной роли, отдать какой-то флаг фронту и при этом, при выполнении запроса напрямую, так же запретить операцию для этой роли.

По поводу шины событий для фронтенда, как вы решали обновление для нескольких открытых вкладок, через BroadcastChannel? Были ли еще какие-то проблемы, например с обновлением jwt токена (если используется, конечно)?

Пользователи у нас хранились во внешней системе — например, Active Directory / OAuth2. Также есть вариант, когда логин/пароль остаются на уровне базы API Gateway, но это касается именно аутентификации.

Список доступных ролей и разрешений пользователя хранится в доменном сервисе прав/разрешений. На практике система прав чуть сложнее, чем просто роли, но суть в том, что источник прав у нас один. Логика расчёта доступов не размазывается между Gateway и разными модулями.

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

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

---

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

JWT у нас хранится в cookies и обновляется по мере выполнения запросов. Отдельных проблем с синхронизацией токена между вкладками из-за этого подхода не возникало.

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

Спасибо большое за статью, очень интересно!
Я не очень понял где все таки проходят границы микросервисов в вашем случае. Находится ли все в модульном монолите (как одна единица деплоймента) или каждый домен это отдельный сервис?
APIGW и BFF деплоятся все вместе как один сервис или раздельно?

У нас есть два слоя: функциональные модули и доменные сервисы.

Доменные сервисы — это отдельные сервисы со своими границами ответственности. А вот со словом “микросервис” я бы был аккуратнее, потому что оно сразу накладывает дополнительные ожидания и требования, хотя они и близки к ним.

Функциональные модули — это логические модули, обычно на уровне отдельных csproj. В идеальном мире каждый такой функциональный модуль можно было бы вынести в отдельный solution. Но на практике так не получится - не хватает ресурсов: появляется слишком много репозиториев, пайплайнов, деплоев, инфраструктурной обвязки и операционной сложности.

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

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

API Gateway у нас — отдельный сервис.

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

Как раз наоборот.

Т.к. система малосвязанная. В старый код (проекты) лезть уже не нужно. Можно спокойно пилить новые страницы новой командой.

Если потребуется правки, то распределённость логики позволит на функциональных модулях вносит изменения без опасения сломать соседние страницы (части системы).

Доменные сервисы тоже не обладают сложной логикой и легче, чем микросервисы для восприятия.

- как спроектировать веб приложение на годы вперёд?

- никак

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации