Границы сервисов: как понять, где резать монолит
Микросервис не начинается с отдельного репозитория. Сначала нужно найти устойчивую границу ответственности, данных и изменений — иначе монолит превратится в распределённый комок зависимостей.

Монолит обычно начинают делить не там, где это уменьшает связность, а там, где команда устала от одного большого репозитория. В результате появляются сервисы, которые синхронно вызывают друг друга, делят одну базу и выпускаются только вместе. Получается не независимая архитектура, а распределённый монолит. Ниже — практический способ найти границы сервисов по бизнес-правилам, данным, изменениям и цене сетевого взаимодействия. Материал написан для систем на актуальных ветках Django 5.x, Spring Boot 3.x и .NET 8; критерии не зависят от конкретного фреймворка.
Резать нужно не таблицы и не папки
Плохой первый вопрос звучит так: «Какой модуль вынести в отдельный сервис?» Хороший — «Какая часть системы может принимать решения и изменяться, почти не согласовываясь с остальными?»
Сервис — это не просто процесс с HTTP API. У него должна быть собственная зона ответственности, понятный владелец и набор данных, за которые он отвечает. Если внешний компонент постоянно просит внутренние таблицы, передаёт десятки полей и ждёт немедленного ответа, граница, скорее всего, проведена слишком рано или не там.
Не стоит начинать с технических слоёв: «сервис авторизации», «сервис репозиториев», «сервис общих моделей». Такие названия описывают устройство кода, но не объясняют, кто принимает бизнес-решение. Сервис «заказы» выглядит содержательно, а сервис «валидация» может оказаться общей библиотекой, а не самостоятельной частью продукта.
Хорошая граница выдерживает изменение внутри сервиса. Плохая граница заставляет два сервиса менять код, контракт и базу данных для одного бизнес-правила.
Четыре теста для будущей границы
1. Кто отвечает за решение
Опишите правило одним предложением: «Система разрешает возврат, если…», «Цена доставки зависит от…», «Пользователь получает доступ, когда…». Затем спросите, какой компонент должен быть единственным источником истины для этого решения.
Если правило размазано по контроллеру, SQL-запросу, фоновой задаче и административной панели, сначала соберите его в одном модуле монолита. Выносить размытое правило в сервис опасно: сетевой API лишь скроет отсутствие владельца.
2. Какие данные меняются вместе
Граница должна учитывать не только чтение, но и запись. Если операция обязана атомарно изменить заказ, резерв товара и остаток пакета, разносить эти данные по сервисам придётся только вместе с новым протоколом согласования. Простое перемещение таблиц проблему не решит.
Проверьте зависимости:
- какие записи создаются в одной транзакции;
- какие ограничения целостности соединяют таблицы;
- какие фоновые задачи обновляют данные после основной операции;
- какие отчёты читают внутренние поля напрямую;
- какие данные можно передавать как событие, а какие нужны только владельцу.
Чем больше обязательных совместных записей, тем выше цена разделения. Это не запрет на декомпозицию, а сигнал, что сначала понадобится изменить модель процесса.
3. Как часто части меняются вместе
История изменений часто показывает границу лучше, чем диаграмма классов. Возьмите последние релизы или коммиты и отметьте, какие каталоги и модули регулярно меняются одной задачей. Если код расчёта цены и код оформления заказа почти всегда правятся вместе, отделять их только по названию рано.
И наоборот, редкие изменения — не доказательство автономности. Модуль может быть стабильным, но критически зависеть от внутренних структур соседей. Смотрите одновременно на совместные изменения, количество потребителей и тип контрактов.
4. Что произойдёт при сбое сети
Внутри процесса вызов функции обычно быстрый и может участвовать в общей транзакции. Между сервисами появляются задержка, таймаут, повтор запроса и частичный успех. Для каждой предполагаемой границы задайте вопрос: «Что увидит пользователь, если зависимый сервис недоступен в этот момент?»
Если ответом может быть отложенная обработка, очередь или повторная попытка — граница реалистична. Если пользователь должен получить синхронный ответ от пяти сервисов, разделение может ухудшить систему даже при аккуратном API.
Термин: bounded context — ограниченный контекст предметной области. Это участок, внутри которого термины, правила и модели имеют согласованное значение. Например, «клиент» в поддержке и «плательщик» в биллинге могут описывать одного человека, но не обязаны быть одной общей сущностью.
Кандидаты на выделение: что сравнивать
Кандидатами обычно становятся не самые крупные модули, а те, где сходятся несколько сигналов: отдельная бизнес-ответственность, независимый темп изменений, ясный владелец данных и понятный режим отказа.
| Кандидат | Признаки хорошей границы | Основная цена | Когда не выносить |
|---|---|---|---|
| Платежи | Отдельные правила, безопасность, внешний провайдер, собственный жизненный цикл операции | Идемпотентность, сверка статусов, обработка вебхуков | Если платёж всегда является частью одной неделимой локальной операции и система ещё не умеет восстанавливаться после сбоев |
| Уведомления | Допустима асинхронная доставка, есть несколько каналов и повторные попытки | Очередь, дедупликация, наблюдаемость доставки | Если уведомление содержит критичное решение, без которого нельзя завершить операцию |
| Поиск | Индекс можно восстановить из источника данных, запросы отличаются от транзакционной модели | Задержка обновления индекса и отдельный жизненный цикл данных | Если поиск напрямую определяет финансовый или юридически значимый результат |
| Каталог | Собственные правила товаров, ценовых атрибутов и публикации, отдельный владелец | Синхронизация доступности, цен и представлений для других частей системы | Если заказ, склад и каталог постоянно меняют одни записи в одной транзакции |
| Отчётность | Нужны тяжёлые запросы и независимое расписание, данные можно получать снимками или событиями | Задержка данных, повторная загрузка, контроль расхождений | Если отчёт должен читать внутреннюю модель в реальном времени без промежуточного слоя |
Таблица не даёт готового ответа. Она помогает сравнить цену границы с её пользой. Вынесенный сервис должен убрать конкретную проблему: ускорить независимый релиз, изолировать отказ, отделить нагрузку или дать команде самостоятельную ответственность.
Как проверить границу до миграции
Сначала — модульный монолит
Самый дешёвый эксперимент — отделить модуль внутри одного приложения. Запретите прямой доступ к его внутренним классам и таблицам, оставьте явный интерфейс, добавьте тесты на публичные сценарии. Пока код работает в одном процессе, вы проверяете смысл границы без сетевых таймаутов и распределённых транзакций.
Если модуль невозможно использовать без внутренних объектов соседей, это полезный результат. Граница ещё не готова. Исправьте зависимости, а не маскируйте их DTO и HTTP-вызовами.
Составьте карту операций
Для каждого сценария запишите последовательность действий: кто принимает команду, какие данные читает, что меняет, какие события публикует и какой ответ нужен пользователю. Отдельно отметьте синхронные и асинхронные шаги.
После этого видны три проблемные зоны:
- транзакция пересекает будущую границу;
- один сервис читает внутреннюю таблицу другого;
- запрос проходит через длинную цепочку синхронных вызовов.
Каждую зону нужно закрыть архитектурным решением: локальной копией данных, событием, сагой или отказом от выделения. Название технологии не заменяет описание поведения.
Запустите «теневой» контракт
До переноса трафика можно заставить новый компонент получать реальные входные данные и сравнивать результат со старой логикой, не влияя на ответ пользователю. Так проверяются расхождения в расчётах, неполные данные и редкие ветки. Такой режим требует маскировки персональных данных и ограничения побочных эффектов: новый компонент не должен отправлять реальные письма или списывать деньги.
Метрики выбирайте по риску границы: доля ошибок и таймаутов, время ответа, число повторов, расхождения результатов, возраст очереди и процент операций, завершённых частично. Сравнивайте показатели до и после на одинаковом объёме трафика, а не по впечатлениям команды.
Что ломается первым
Общая база данных
Два сервиса с общей схемой выглядят независимыми только на диаграмме. Любое переименование столбца становится координацией нескольких релизов, а ручной SQL-запрос к чужой таблице превращается в неявный контракт.
На переходном этапе общая база допустима, если вы явно фиксируете владельца каждой таблицы, запрещаете записи из чужого кода и планируете миграцию чтения. Но это временная мера, а не готовая граница.
Синхронная цепочка
Цепочка «заказ вызывает каталог, каталог вызывает цены, цены вызывают скидки» переносит связанность из кода в сеть. Таймаут одного узла начинает выглядеть как случайная ошибка другого. Для каждого вызова нужны ограничение времени ожидания, понятная политика повторов и безопасная обработка повторной доставки.
Общие модели и события
Общий пакет моделей ускоряет старт, но связывает версии сервисов. Общий поток событий создаёт похожую проблему: если каждое изменение сущности публикуется для всех подписчиков, никто не понимает, какой контракт действительно стабилен.
Публикуйте события о факте, который важен потребителю, а не дамп внутреннего объекта. Например, «платёж подтверждён» устойчивее, чем событие со всеми полями внутренней таблицы платежа.
Когда резать не нужно
Отложите декомпозицию, если основная боль не связана с границами. Медленный запрос лечится профилированием и индексами, длинный релиз — автоматизацией сборки и тестов, конфликт в команде — распределением ответственности и правилами владения кодом.
Не выделяйте сервис ради количества репозиториев, модного стека или обещания «масштабировать независимо». У независимого масштабирования должна быть конкретная причина: разные профили нагрузки, разные требования к доступности или изоляция ресурсоёмкой операции.
Отдельно стоит проверить организационную цену. Если один разработчик должен поддерживать десять маленьких сервисов, а для каждого нужны мониторинг, доставка, секреты, резервирование и журналирование, выигрыш от разделения может исчезнуть.
Что делать дальше
Выберите один кандидат с ясным владельцем и ограничьте эксперимент одной бизнес-операцией. Зафиксируйте текущий путь запроса, измерьте задержку и ошибки, затем оформите границу как модуль внутри монолита. Только после этого добавляйте сеть, отдельное хранилище или очередь.
В проде смотрите не только на доступность нового сервиса. Проверяйте расхождения данных, долю повторных операций, возраст сообщений в очереди, число обращений к чужому хранилищу и время восстановления после отказа. Инвалидация кеша и выбор конкретного брокера сообщений — отдельные темы; здесь важнее доказать, что граница выдерживает изменение и сбой.
Инфобез и 152-ФЗ: Аудит политик конфиденциальности, правила сбора cookie.
Спросить за 20 ₽Источники: MonolithFirst — Martin Fowler, Designing a microservices architecture — Microsoft Learn
Частые вопросы
Нужно ли сначала разделить монолит на модули, а потом на сервисы?
Можно ли нескольким сервисам использовать одну базу данных?
Как понять, что сервис получился слишком маленьким?
Что делать, если после разделения данные обновляются не сразу?
Нужны ли отдельные команды для каждого сервиса?
А если после выделения новый сервис недоступен?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.