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

Почему монолит кажется медленным и рискованным
Монолит не становится медленным только потому, что это один кодовая база. Многие команды годами выпускают продукт из одного приложения и справляются отлично. Они двигаются быстро, потому что у каждой части продукта есть понятный владелец. Одна команда отвечает за billing, другая — за auth, третья — за admin area. Люди знают, кто может что менять, кто это проверяет и кто отвечает, когда что-то ломается.
Проблемы начинаются, когда это владение становится размытым. Небольшое изменение на одном экране затрагивает общий код, потом его смотрят три команды, и в итоге никто не чувствует себя уверенно, чтобы одобрить деплой. Сам код может быть несложно править. А вот путь в продакшен начинает казаться забитым.
Часто команды в первую очередь винят архитектуру. Говорят, что монолит слишком большой, слишком старый или слишком запутанный. Иногда это правда. Но не реже главная проблема в том, что у модуля нет чёткого хозяина. Когда владение размыто, любое изменение превращается в групповое решение, а такие решения идут медленно.
Размер кода важен меньше, чем ясность владения. Большой монолит с аккуратными границами модулей может двигаться быстрее, чем маленькая система, разрезанная на сервисы, которыми понемногу владеют сразу несколько человек. Когда одна команда может менять, тестировать и выпускать свою зону без согласования с пятью другими, монолит перестаёт казаться ловушкой.
Возьмём баг в checkout, который нужно исправить одной строкой. Инженер вносит правку за десять минут. Потом pull request ждёт ревью от payments, platform и QA, потому что код лежит в общей папке, которой пользуются все, но не владеет никто. Когда он наконец выходит в релиз, команда говорит: «Нам нужны микросервисы». Но репозиторий — не первая проблема. Команда так и не решила, кто контролирует checkout.
Именно поэтому боль от монолита часто ощущается как технический долг, хотя на деле она чаще связана с устройством команды. Если права на деплой неясны, тесты затрагивают всё приложение, а модули пересекают несколько команд, монолит будет казаться рискованным, как бы аккуратно ни выглядел код.
Признаки, что у вас проблема с владением
Сначала такие проблемы редко выглядят драматично. Продукт работает, релизы выходят. Но обычные изменения начинают ощущаться медленными, рискованными и странно политизированными.
Один из признаков — пересечение. Две команды меняют один и тот же модуль на одной неделе не потому, что вместе планировали работу, а потому что никто не видит чёткой границы вокруг этого кода. Появляются конфликты при слиянии, регрессии и напряжённые передачи задач. Монолит тут только часть истории, а глубинная проблема в том, что эта зона не закреплена за одной командой.
Деплой быстро показывает ту же проблему. Задайте простой вопрос: кто утверждает production-отправку для этой части системы? Если ответ превращается в длинную переписку, владение размыто. Команды ждут одного человека, который «обычно знает эту часть», или перекладывают решение на менеджера, которому не нужно угадывать.
Тесты тоже многое показывают. Небольшое изменение в одном модуле должно оставаться небольшим. Если правка формы, запроса или одного бизнес-правила будит большую часть тестового набора, код слишком спутан, чтобы владение было понятным. Люди перестают вносить точечные исправления, потому что ничего уже не кажется точечным.
Похожая картина и с баг-репортами. Поддержка отправляет проблему команде A. Команда A говорит, что баг в общей логике, и передаёт его команде B. Команда B находит побочный эффект в другом модуле и возвращает всё обратно. Через два дня клиент всё ещё ждёт, а полный фикс не принадлежит никому.
Даже названия могут выдать проблему. В модуль с названием «billing» внезапно попадают permissions, emails, admin settings и случайный код подписок. Папка с названием «core» обычно означает, что команда перестала соглашаться, что куда относится. Когда названия уже не совпадают с тем, что лежит внутри, владение давно ускользнуло.
Полезна простая проверка. Возьмите один модуль и спросите трёх человек, что он делает, кто проверяет изменения, кто может его деплоить и какие тесты должны пройти. Если ответы не совпадают, разделение этого кода на сервис мало что изменит. Та же путаница просто разойдётся по большему числу частей.
Сначала исправьте права на деплой
Когда релизы идут медленно, команды часто винят монолит. Но во многих случаях первый блокер проще: никто не понимает, кто может выпускать изменения, кто может откатить неудачный релиз и кто может менять настройки в продакшене без лишних вопросов.
Начните с простой карты владения для каждой зоны продукта. Для каждого модуля укажите одну команду, которая принимает решение о релизе. Затем запишите, кто может сливать код, деплоить его, откатывать и менять конфигурацию. Если эти ответы живут в истории чатов или в голове одного старшего инженера, вы будете снова и снова упираться в одну и ту же стену.
Одна команда на одну зону важнее, чем думает большинство. Если checkout принадлежит двум командам, обычно он не принадлежит ни одной. Общая ответственность звучит справедливо, но она замедляет решения и размазывает вину, когда что-то ломается. Дайте одной команде финальное слово по каждой части монолита, даже если другие команды всё ещё вносят код.
Общие библиотеки и изменения в базе данных требуют простых правил. Сделайте их скучными. Изменение общей библиотеки может требовать одобрения владельца модуля, которого оно затрагивает. Изменение базы данных, затрагивающее больше одного модуля, может требовать письменный план отката. Для многих команд этого уже достаточно.
Сделайте аварийный доступ узким и заметным. Не раздавайте широкие права на деплой или конфигурацию половине компании «на всякий случай». Лучше короткий список. Пусть каждое аварийное действие легко отслеживается, а на следующий день его можно спокойно разобрать, пока детали ещё свежи.
После каждого инцидента проверяйте, совпадает ли список владельцев с реальностью. Если звали не тех людей, если никто не чувствовал себя уверенно, откатывая релиз, или если три команды спорили из-за переключателя конфигурации, исправьте это до того, как говорить о разделении сервисов.
Многие проблемы монолита выглядят техническими со стороны. Но часто это проблемы с доступами, просто обёрнутые в код.
Сократите охват тестов до размера модуля
Монолит начинает пугать, когда любое изменение будит всю тестовую систему. Если маленькая правка в billing запускает тысячи тестов по login, search, admin и reporting, проблема не только в скорости. Это ещё и вопрос владения. Никто не может сказать, кто именно сломал что, поэтому каждый сбой воспринимается как общая погода.
Сначала разделите тесты по модулям, а уже потом думайте о фреймворках. У каждого модуля должен быть свой быстрый набор тестов, который проверяет правила, доступ к данным и публичное поведение. Вся система по-прежнему нуждается в сквозном покрытии, но этот набор должен оставаться небольшим и фокусироваться на нескольких важных сценариях.
Хорошо работает простая схема:
- Модульные тесты запускаются на каждом коммите.
- Контрактные или API-тесты проверяют, как модули общаются между собой.
- Короткие smoke-тесты всего приложения запускаются реже — например, перед релизом или по расписанию.
Это экономит время, но ещё важнее — даёт ясность. Когда модуль orders падает на своём наборе тестов, команда orders понимает, что это её фикc. Когда красным становится один огромный билд, все просто смотрят на него и ждут.
Сквозные тесты между модулями создают много шума. Команды добавляют их с хорошими намерениями, а потом забывают. Через несколько месяцев изменение в user profiles ломает тест для reporting, который ещё задевает permissions, email и exports. Такой тест больше не говорит вам ничего полезного. Он говорит пять расплывчатых вещей и плохо. Удалите его или разбейте на модульные тесты плюс одну узкую проверку интеграции.
Названия тестов тоже важны. Если по падающему тесту неясно, какой модуль отвечает за проблему, на него сложнее реагировать. Чёткий охват тестов даёт командам более быструю обратную связь и более ясную ответственность.
Сделайте границы модулей такими, чтобы их можно было удержать
Большая часть проблем с владением начинается задолго до разделения на сервисы. Код растёт вокруг фреймворков, и в итоге у команд появляются папки вроде controllers, services, utils и models. Сначала это выглядит аккуратно, но потом скрывает реальную бизнес-задачу.
Группируйте код по тому, что делает продукт, а не по слою, в котором он лежит. Модуль должен соответствовать чему-то, что может назвать даже неинженер: billing, onboarding, pricing, reporting, inventory. Если команда может описать модуль, не упоминая стек технологий, такую границу обычно проще удержать.
Названия важнее, чем многие готовы признать. Backend, core, shared или misc не подсказывают, где именно должно лежать изменение. А вот subscription billing или customer import — подсказывают. Переименование размытых папок и пакетов кажется мелочью, но оно убирает массу ежедневной путаницы.
У каждого модуля также должен быть понятный вход и понятный выход. Остальные части приложения должны знать, как его вызвать, какие данные можно отправить и что вернётся в ответ. Если разработчикам приходится лезть в приватные файлы или копировать внутреннюю логику, граница не настоящая.
Доступ к базе данных — то место, где многие монолиты ломаются. Один модуль читает таблицы другого, пишет пару колонок, а потом добавляет обходной путь, потому что так быстрее. Через месяц уже никто не знает, кто владеет правилом. По возможности держите прямой доступ к данным внутри того модуля, которому эти данные принадлежат. Пусть другие модули обращаются через код, а не тянутся через границу запросами.
Уборка не обязана быть драматичной. Переименуйте папки, которые никто не может объяснить. Перенесите код рядом с той бизнес-задачей, которую он поддерживает. Уменьшите межмодульные записи в базу. Оставьте для каждого модуля небольшой публичный API. Потом напишите одну фразу, которая начинается со слов: «Этот модуль отвечает за...»
Эта фраза делает больше, чем кажется. «Этот модуль отвечает за изменения тарифов и состояние биллинга для активных подписок» — это ясно. Если команда не может написать такую фразу, граница всё ещё размыта, и разбиение на сервисы только разнесёт беспорядок по разным местам.
Простой пример из растущего продукта
Checkout — это место, где многие растущие SaaS-продукты спотыкаются первыми. Клиент платит картой, получает неверную цену, не получает письмо с квитанцией и случайно запускает проверку на fraud. Код этого потока живёт в одном монолите, но к нему прикасаются четыре команды. Pricing меняет одно правило, billing — другое, marketing правит время письма, а platform-команда утверждает деплой.
Со стороны это выглядит как техническая проблема, но по сути это проблема владения. Никто не отвечает за результат целиком, поэтому любое изменение превращается в передачи задач, ожидание и поиск виноватых.
Лучшее решение меньше, чем разделение на сервисы. Дайте одной команде полную ответственность за изменения в pricing и billing от начала до конца. Не обязательно, чтобы эта команда навсегда владела всем checkout. Ей нужна понятная зона, где она может вносить изменения, запускать тесты и выпускать релиз без разрешения трёх других групп.
Затем перенесите охват тестов ближе к этой зоне. Вместо огромного набора, который трогает весь продукт при каждом изменении цены, оставьте сфокусированный набор тестов для итогов checkout, успешных и неуспешных платежей, писем с квитанциями и флагов fraud. Проверка релиза должна отвечать на один вопрос: сломало ли это изменение путь pricing и billing?
Остальная часть монолита может остаться на месте. Вам не нужны новые сетевые вызовы, новые пайплайны деплоя или новая схема дежурств только для того, чтобы исправить неясное владение. Нужны более чёткие границы модулей, более быстрые права на деплой и тесты, которые соответствуют той зоне, которой команда действительно управляет.
Если координация всё ещё болезненна после этого, разделение на сервисы может стать разумным позже. Тогда это уже безопаснее, потому что граница владения будет видна и в коде, и в оргструктуре. Это намного лучший момент для разреза.
Практический план на следующий месяц
Одного месяца достаточно, чтобы понять, нужен ли вам новый сервис или лучшее владение внутри монолита. Держите фокус узким. Выберите одну болезненную зону — например, checkout, user accounts или reporting — и используйте её как тестовый пример.
На первой неделе сделайте карту. Запишите, какие модули участвуют, кто их меняет, кто утверждает деплой и кто может откатить неудачный релиз. Уложите это на одну страницу. Если три человека могут выпускать изменения, но никто не чувствует ответственности, когда что-то ломается, вы нашли реальный пробел во владении.
На второй неделе поправьте охват тестов. Разделите быстрые модульные тесты и медленные сквозные. Разработчик должен менять эту зону и получать полезную обратную связь за минуты, а не ждать неродственных проверок по всему коду. Если одно маленькое изменение всё ещё требует полного прогонки всех тестов, проблема не в нехватке микросервисов.
На третьей неделе сделайте границу реальной. Найдите вызовы между модулями, которые появились только потому, что код вырос именно так со временем. Спрячьте общую логику за чёткими точками входа. Уберите обходные пути в другие модули. Перестаньте снаружи лезть во внутренние структуры данных. Одна команда должна иметь возможность менять эту зону, не трогая три соседние части приложения.
На четвёртой неделе проведите тренировочный деплой. Владелец зоны должен в рабочее время пройти обычный деплой и откат. Засеките время. Посмотрите, кому приходится подключаться, какие проверки всё ещё зависят от знаний «в головах людей» и где процесс застревает. Разделение на сервисы не лечит запутанные права на деплой. Часто оно только делает их менее заметными.
После этого принимайте решение по фактам. Если у зоны есть один владелец, быстрые тесты, понятные границы и спокойный путь релиза, спросите себя, решит ли отдельный сервис реальную проблему — например, разный ритм релизов или разные требования к масштабированию. Если эти базовые вещи всё ещё хромают, сначала продолжайте чинить монолит. Раннее разделение просто переносит путаницу в сеть.
Ошибки, которые команды допускают, когда делят слишком рано
Часто команды делят сервис, чтобы уйти от споров внутри монолита. Обычно это не помогает. Та же проблема владения всплывает уже после разделения, только теперь она ломается через сеть.
Первая ошибка простая: новым сервисом никто не владеет. Одна команда его создаёт, две другие от него зависят, и все считают, что поддерживать его будет кто-то другой. Мелкие баги лежат по несколько дней. Срочные изменения превращаются в споры в Slack. Если одна команда не может сказать «это наше» и подтвердить это правами на деплой, временем в backlog и дежурством, сервис ещё не готов к самостоятельной жизни.
Другая ошибка — сразу спрятать общие таблицы за API. На бумаге это выглядит чище. На практике та же запутанная модель данных по-прежнему управляет всем, только теперь каждый запрос на чтение и запись требует удалённого вызова. Модуль checkout, который раньше читал одну таблицу, теперь ждёт новый сервис, повторяет запросы при таймаутах и ломается там, где раньше всё было скучно и быстро.
Команды ещё и копируют огромный набор тестов в каждый новый сервис. Неделю это кажется безопасным, а потом любое маленькое изменение запускает длинные пайплайны в нескольких репозиториях. Люди перестают доверять тестам, потому что шума становится слишком много. Новому сервису нужен меньший охват тестов, а не весь багаж монолита.
API-запросы тоже не решают конфликт между командами. Если две команды не могут договориться о правилах данных, сроках релиза или приоритетах багов внутри одной кодовой базы, они не начнут соглашаться только потому, что граница теперь использует JSON. Конфликт просто станет сложнее отследить.
Чаще всего забывают про операционную работу. Новому сервису нужен тот, кто будет носить pager, определять шаги отката, разбирать инциденты и postmortem, а также делать алерты полезными, а не шумными. Если эти обязанности всё ещё размыты, лучше подождать.
Разделение на сервисы должно уменьшать путаницу. Если оно распыляет путаницу по большему числу репозиториев, деплоев и людей, разделили слишком рано.
Короткая проверка перед созданием сервиса
Новый сервис помогает только тогда, когда одна команда может вести его от начала до конца. Если код уходит из монолита, а владение остаётся размытым, вы просто переносите ту же задержку в новое место.
Перед разделением используйте короткую проверку:
- Одна команда может объяснить, чем она владеет, одной чёткой фразой.
- Эта команда может деплоить сама, не ожидая две другие команды.
- Тесты для этой зоны завершаются быстро, а сбои указывают на один модуль, а не на полприложения.
- Модуль имеет небольшую публичную поверхность и прячет большую часть внутренностей.
- Разделение убирает передачи задач и цепочки согласований, а не просто перекладывает файлы.
Если хотя бы один пункт хромает, остановитесь. Сначала почините границу команды. Потом исправьте права на деплой. Затем уменьшайте охват тестов, пока команда не начнёт быстро получать обратную связь от собственного кода.
Один плохой признак выглядит так: команда говорит, что владеет billing, но каждый релиз всё равно требует помощи platform для смены конфигурации, помощи auth для общего кода и полной регрессии, потому что тесты задевают чужие зоны. Эта команда ещё не владеет billing. Она владеет очередью тикетов.
Границы модулей важны не меньше. Когда модуль прячет внутренности, другие части приложения зависят от нескольких понятных точек входа. Это делает баги проще для поиска, а изменения — безопаснее для выпуска. Если другие команды лезут в его таблицы, классы или helper-функции, разделение на сервис принесёт больше боли, чем пользы.
Размер кода сам по себе — слабая причина для разделения. Большой модуль с понятным владением часто проще сопровождать, чем маленький сервис, для изменения которого нужны три команды. Создавайте сервис, когда это действительно сокращает координацию. Если он просто меняет место, где лежит код, оставьте его в монолите и сначала приведите в порядок границы.
Что делать дальше, если команда всё ещё буксует
Большинству проблем монолита сначала не нужен новый сервис. Им нужен один модуль с понятным владельцем, понятным путём деплоя и тестами, которые остаются внутри этого модуля.
Выберите часть продукта, которая создаёт больше всего трения при релизах. Назначьте одного человека напрямую отвечать за неё на ближайший месяц. Это не значит, что он пишет весь код. Это значит, что он утверждает изменения, следит за тестами и принимает финальное решение о готовности к релизу.
Первый шаг держите небольшим. Выберите один модуль с частыми изменениями, дайте ему одного владельца или небольшую пару, опишите один путь деплоя для этого модуля и запишите, какие тесты должны пройти перед релизом.
Затем используйте эти правила в реальных релизах. Правило, которое живёт только в документе, бесполезно. Если владельцу по-прежнему нужны три согласования от не связанных команд, вы не исправили права на деплой. Если для крошечного изменения по-прежнему запускаются все тесты репозитория, вы не уменьшили охват тестов. Если люди всё ещё спорят, где модуль начинается и заканчивается, граница всё ещё размыта.
Запишите, что происходит после двух-трёх релизов. Отметьте, где команда ждала, кто блокировал изменение и какие тесты замедляли процесс. Эти заметки важнее, чем очередной спор об архитектуре.
Если одни и те же вопросы о границах возвращаются снова и снова, поможет внешний взгляд. Oleg Sotnikov на oleg.is работает как Fractional CTO для стартапов и малого бизнеса и помогает командам разобраться с архитектурой, релизным потоком и владением, прежде чем они добавят ещё больше сервисов. Короткий разбор часто обходится дешевле, чем недели переноса кода в разделение, которое сохранит те же человеческие проблемы.
Часто задаваемые вопросы
Монолит — это правда проблема?
Не всегда. Многие монолиты годами работают хорошо, если за каждой частью продукта закреплена одна команда, и она может менять, тестировать и выпускать изменения без длинной цепочки согласований.
Если ради каждого маленького исправления нужны несколько команд, широкие прогонки тестов или неясное разрешение на деплой, проблема чаще в организации работы вокруг кода, а не в самом виде репозитория.
Почему монолит начинает казаться медленным и рискованным?
Обычно причина в размытом владении. Маленькое изменение затрагивает общий код, его смотрят несколько людей, и никто не чувствует себя уверенно, нажимая на деплой.
Из-за этого обычная работа становится медленной и политизированной. Код может выглядеть нормально, но путь в продакшен кажется забитым.
Как понять, что у нас проблема с владением?
Выберите один модуль и спросите трёх человек о четырёх вещах: что он делает, кто проверяет изменения, кто может его деплоить и какие тесты должны пройти.
Если ответы не совпадают, владение неясно. Ещё один признак — когда тикеты от поддержки прыгают между командами или согласование деплоя превращается в длинную переписку.
Могут ли два отдела владеть одним модулем?
Нет. Для каждой зоны нужна одна команда с финальным словом. Другие команды могут участвовать в работе, но именно один владелец должен решать вопросы изменений, релизов и отката.
Если финальная ответственность делится между двумя командами, решения замедляются, а при сбоях быстро начинается перекладывание вины.
Что нужно исправить перед переходом на микросервисы?
Начните с прав на деплой, охвата тестов и границ модулей. Зафиксируйте, кто может сливать изменения, деплоить, откатывать и менять конфигурацию в самой проблемной зоне.
Затем подгоните тесты под эту зону и приведите границы в порядок, чтобы другие части приложения перестали залезать в её внутренности. Разделение на сервисы лучше работает уже после этого.
Как стоит менять тесты в монолите?
Держите большую часть тестов рядом с тем модулем, который менялся. Маленькое изменение в billing не должно будить login, search, admin и reporting, если оно действительно их не затрагивает.
Запускайте быстрые модульные тесты на каждый коммит, держите несколько точечных проверок между модулями и используйте небольшой набор smoke-тестов для всего приложения перед релизом. Так команда быстрее получает обратную связь и лучше понимает ответственность.
Как выглядит хорошая граница модуля?
Хорошая граница следует за бизнес-задачей, а не за слоем фреймворка. Названия вроде billing, onboarding, pricing или reporting дают команде более понятную цель, чем folders вроде core или utils.
У модуля должна быть небольшая публичная поверхность, а его правила и доступ к данным лучше держать внутри. Если другим командам нужны приватные файлы, вспомогательные функции или прямые записи в таблицы, граница слишком слабая.
Когда разделение на сервисы действительно имеет смысл?
Разделяйте тогда, когда одна команда уже может полностью отвечать за область, а разделение реально убирает координационную боль. Хорошие причины — разный ритм релизов, разные требования к масштабированию или граница, которую команда и так уже соблюдает внутри монолита.
Не делите только потому, что код кажется большим. Большой код с понятным владением часто проще сопровождать, чем маленький сервис, для изменения которого нужны три команды.
Что идёт не так, когда команды делят сервис слишком рано?
Часто команды слишком рано переносят общие данные за API, копируют огромные наборы тестов в новые репозитории и забывают, кто будет дежурить и делать деплой. В итоге одна форма путаницы просто меняется на другую.
Конфликт между командами тоже никуда не исчезает. Если люди не могут договориться о правилах или сроках релиза в одном кодовой базе, JSON по сети это не исправит.
Когда стоит обратиться за внешней помощью?
Привлекайте внешнюю помощь, если одна и та же проблема с релизами возвращается снова и снова после нескольких реальных исправлений. Если права на деплой всё ещё неясны, тесты по-прежнему охватывают всё приложение, или команды спорят, где начинается и заканчивается модуль, свежий взгляд сэкономит время.
Короткий разбор архитектуры и владения обычно стоит меньше, чем перенос кода в новые сервисы, которые сохраняют те же человеческие проблемы.