Модульный монолит против микросервисов: критерии перехода без религиозных войн
«Микросервисы» в современных командах часто превращаются в религиозный термин. На стартапе из пяти человек поднимают пятнадцать сервисов с своими репозиториями, своими CI и своими очередями, потому что «так делают взрослые». Через год команда тратит больше времени на инфраструктуру и интеграцию, чем на продуктовую разработку. С другой стороны, в зрелой компании на сорок инженеров одна большая монолитная кодовая база с десятками команд тоже превращает любую релизную пятницу в стресс.
Истина — где-то посередине, и это «где-то» зависит от вашего конкретного контекста. Расскажу, как я обычно выбираю между модульным монолитом и микросервисами, и какие критерии работают лучше «у нас же масштаб».
Что такое модульный монолит на самом деле
Модульный монолит — не «большой бесформенный комок кода». Это одно приложение, в котором внутренние границы проведены явно: модули с собственной структурой, своими доменными моделями, своими портами наружу. Внутри одного процесса, в одной базе или с разными схемами в одной БД.
Ключевое отличие от микросервисов: компонент развертывается как единое целое и общается внутри без сетевых вызовов. Ключевое отличие от «классического монолита»: у каждого модуля есть владелец, граница и контракт.
myapp/
modules/
catalog/ # своя модель, свои таблицы
domain/
api/ # публичные интерфейсы для других модулей
internal/ # детали реализации, недоступны извне
orders/
billing/
notifications/
shared/
eventbus/
persistence/В Java/Kotlin это естественно делается через Gradle/Maven-модули с jigsaw или явными правилами. В Go — через директории и go-imports. В .NET — через проекты в одном решении.
Что вы получаете от микросервисов и за какую цену
Микросервисы дают:
- Независимый деплой: каждая команда катит свой сервис когда хочет.
- Изоляцию по отказам и нагрузке: один тяжёлый сервис не валит остальных.
- Технологический выбор: разные стеки в разных сервисах, если действительно нужно.
- Масштабируемость по командам: сорок человек не толпятся в одном репозитории.
Платите вы за это:
- Сетью между сервисами: timeout-ы, retry, circuit breakers.
- Распределёнными транзакциями: saga, outbox, eventual consistency.
- Эксплуатацией: десять Docker-образов, десять деплоев, десять наборов метрик.
- Сложностью отладки: трейсинг, корреляция логов, воспроизведение бага в локальной среде.
- Стоимостью разработки: каждое изменение, проходящее через несколько сервисов, требует согласования.
Если перечисленные выгоды для вас не критичны прямо сейчас — вы платите цену, не получая отдачи.
Когда модульный монолит — нормальный выбор
- Команда меньше 20 человек. Разделение на 3–5 команд внутри одного монолита с чёткими модулями — рабочий вариант. Релизный конфликт легко разруливается, миграции БД — линейные.
- Один продукт, понятный домен. Если все ваши сервисы говорят про один и тот же бизнес (один e-commerce, один SaaS-продукт), монолит логичен.
- Релиз раз в день или реже. Независимый деплой не критичен, если вы и так релизитесь по графику.
- Нет принципиально разной нагрузки между модулями. Если катаете всё одним пакетом и всё держит нагрузку, нет смысла раскидывать.
Я наблюдал успешные модульные монолиты, обслуживающие десятки тысяч RPS, в нагруженных проектах. Это не «мелкие пет-проекты» — это просто другой способ организации.
Когда пора резать на сервисы
Главный сигнал — не «технический», а организационный: команды начинают мешать друг другу. Конкретно это видно по:
- Релизные конфликты. Команда A не может выкатить фичу, потому что команда B не закончила свою. Каждую неделю — встреча по координации релиза.
- Конфликт миграций БД. Несколько команд одновременно меняют схему, и кто-то всегда последний.
- Разная нагрузка. Один модуль обрабатывает миллионы запросов в день, другой — тысячи. Ресурсы тратятся неравномерно, масштабировать всё разом дорого.
- Разные требования к доступности. Биллинг должен быть доступен 99,99%, отчёты — 99%. В одном процессе это сложно обеспечить.
- Разные релизные циклы. Каталог обновляется каждый день, биллинг — раз в месяц после долгого тестирования. Совмещать в одном процессе неудобно.
Когда два-три из этих признаков совпадают — пора начинать. Не сразу резать всё, а выделить один-два самых проблемных модуля.
Постепенный переход
Я ни разу не видел успешной «большой переписки монолита на микросервисы за квартал». Видел много неуспешных. Рабочий путь — постепенный, шаг за шагом.
Шаг 1: Привести монолит в форму
Если кодовая база — каша без модулей, начинать с микросервисов бессмысленно: вы получите кашу с сетью внутри. Сначала наведите порядок: выделите модули, проведите границы, ограничьте обращения через явные интерфейсы.
На этом этапе хороший показатель — модуль orders не должен делать SELECT из таблиц модуля catalog. Только через интерфейс. Если этого добились — у вас готов фундамент для разрезания.
Шаг 2: Выделить первый сервис
Берите модуль с самой явной болью (релизные конфликты, разная нагрузка) и выделяйте в отдельный сервис. Связь с остальным монолитом — через HTTP или события. Одна команда отвечает за выделение, монолит остаётся.
На этом этапе появляется первая инфраструктура: API gateway или внутренний роутинг, мониторинг, контракты. Это разовая работа, которую вы делаете для первого сервиса и потом переиспользуете.
Шаг 3: Жить так какое-то время
После выделения первого сервиса — пауза. Поймите, как с ним жить: как мониторить, как деплоить, как восстанавливаться при инцидентах. Если на этом этапе становится больно — стоп, дальше не идём, исправляем процессы.
Если всё нормально — выделяем следующий по приоритету. Тоже один. Так появляется ритм: 2–4 сервиса в год, не двадцать за квартал.
Антипаттерны
Распределённый монолит
Самое популярное недоразумение. У вас десять «микросервисов», но релизятся они всегда вместе, потому что любое изменение требует синхронных правок в нескольких. Это монолит, разделённый по сети, и он наследует все недостатки монолита плюс все недостатки распределённой системы.
Признак: чтобы выкатить одну фичу, нужно скоординированно обновить три-пять сервисов. Лекарство: пересмотреть границы. Возможно, эти сервисы должны быть одним.
Микросервисы по техническим слоям
«Сервис аутентификации», «сервис нотификаций», «сервис orchestration». Если такие имена соседствуют с «сервис заказов», у вас смешаны два принципа разделения: бизнесовый и технический. Это ведёт к тому, что каждое бизнес-действие проходит через два-три технических сервиса, и трассировка превращается в детектив.
Лучше — разделение по бизнес-доменам. Аутентификация и нотификации — это библиотеки или инфраструктурные сервисы, не бизнес-сервисы.
Сервисы из-за «команды этой страны»
Иногда сервисы создаются по организационной причине: «эта команда сидит в другом офисе, проще дать им свой сервис». Иногда это нормально, иногда — повод сделать модуль с чётким владением, не отдельный сервис. Если бизнес-домен не отличается, отдельный сервис тут — лишний накладной расход.
Сравнительные критерии в одном месте
- Команда: до 20 — обычно монолит хватает; больше — задумайтесь.
- Релизы: один общий темп — монолит; разные темпы — сервисы.
- Нагрузка: однородная — монолит; неоднородная по модулям — повод выделить нагруженные.
- Доступность: единый SLA — монолит; разные требования — повод разделить.
- Технологии: одна — монолит; реальная (не прихоть) потребность в разных стеках — сервисы.
- Эксплуатационная зрелость: нет SRE/DevOps — оставайтесь в монолите дольше; есть — раскрывает возможности микросервисов.
Что запомнить
Модульный монолит и микросервисы — это не лестница, а спектр. На разных этапах жизни системы оптимум разный, и зрелые команды двигаются по этому спектру в обе стороны: иногда выделяют сервис, иногда сливают два обратно. Выбор стоит делать по реальным проблемам, а не по образу «правильной архитектуры». Если у вас сейчас нет конкретной проблемы, которую микросервисы решают — оставайтесь в монолите и наводите в нём порядок. Это сэкономит вам месяцы инфраструктурной работы и позволит сосредоточиться на продукте.