21 сент. 2025 г.·7 мин чтения

Границы модулей в одном коде при упрощённых релизах

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

Границы модулей в одном коде при упрощённых релизах

Почему один код быстро превращается в хаос

Большинство кодовых баз не ломаются из‑за одного ужасного решения. Они приходят в такое состояние постепенно.

Для стартапа или небольшой продуктовой команды единый деплой часто — правильный выбор на раннем этапе. Он упрощает релизы и уменьшает операционные расходы. Проблемы начинаются, когда сроки поджимают. Люди ставят фичу туда, где код уже кажется знакомым, а не туда, где ей на самом деле место. Кто‑то видит нужные данные в другой части, импортирует их, выпускает изменение и идёт дальше.

Этот паттерн повторяется. Вскоре бизнес‑области, которые должны оставаться раздельными, начинают просачиваться друг в друга. Помощник, написанный для ценообразования, появляется в настройках аккаунта. Логика управления пользователями всплывает в отчётности. Папки могут выглядеть аккуратно, но правила уже протекают.

Потом начинаются странные баги. Разработчик меняет валидацию для одного экрана — и фоновая задача падает. Кто‑то обновляет общую функцию — и ломается поток писем. Правка маленькая, но никто не видит всего, что теперь от неё зависит.

Скрытые зависимости наносят настоящий урон. Релиз по‑прежнему может выглядеть простым, потому что есть только одна кнопка деплоя. Но сам деплой уже не предсказуем. Один недокументированный импорт или тихая зависимость от внутренней логики способны превратить рутинное изменение в откат, ночной фикс или долгую ручную проверку.

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

Запутанный монолит редко выглядит так сразу. Он кажется продуктивным до того момента, когда релиз перестаёт быть безопасным.

Что дают границы модулей

Хорошие границы модулей дают каждой бизнес‑области явный дом. Когда у кода есть очевидное место, люди перестают спрашивать: "Куда это положить?" Это звучит мелко, но предотвращает много хаоса. Заказы остаются с правилами заказов. Биллинг — с правилами биллинга. Инструменты поддержки — с инструментами поддержки.

Границы также контролируют, кто с кем может общаться. Один модуль не должен лезть в приватные файлы другого просто потому, что так удобнее. Он должен идти через небольшой публичный вход, например сервис, интерфейс или набор функций, которые другой модуль решил экспонировать. Это сохраняет внутренние детали приватными, чтобы одна команда могла менять их без ломки не‑смежной работы.

На практике границы делают четыре вещи:

  • Разделяют бизнес‑правила по доменам, а не по случайным названиям папок.
  • Ограничивают зависимости, чтобы одно изменение не распространялось по всему приложению.
  • Упрощают ревью, потому что читатель видит, что куда принадлежит.
  • Позволяют сохранить один деплой, не превратив код в клубок.

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

Лучшие границы сначала кажутся слегка раздражающими. Разработчик хочет импортировать внутренний хелпер из другой области и двигаться дальше. Граница говорит «нет». Это полезное трение. Оно заставляет пройти чистый путь и часто обнаруживает смешанные ответственности.

Малые команды получают самый большой эффект от этого. Они обычно хотят один репозиторий, простые релизы и меньше оперативной нагрузки. Строгие правила зависимостей позволяют сохранить такую структуру, не скатившись в запутанный код. Результат скучный в хорошем смысле: меньше неожиданных поломок, яснее ответственность и локализованные изменения.

Начинайте с доменов, а не с папок

Если вы хотите чистые границы внутри одного кода, начните с бизнеса. Спросите, какие задачи софт делает каждый день. "Accounts", "billing" и "notifications" — полезные имена модулей, потому что они соответствуют реальной работе. "Controllers", "services" и "utils" — нет. Эти имена описывают форму кода, а не ответственность.

Дерево папок по техническим слоям выглядит аккуратно неделю, а затем превращается в поиск по мусору. Одно изменение в биллинге растянется по контроллерам, моделям, хелперам и общему коду. Люди перестают видеть, где начинается и кончается биллинг. Так код становится трудным для изменений, даже если всё ещё находится в одном репозитории.

Группируйте файлы по работе, которую они поддерживают. Если файл существует, чтобы помочь биллингу снять деньги с клиента, он принадлежит billing. Если он отправляет почту аккаунта — он в accounts или notifications, в зависимости от того, кто владеет этим поведением. Простое правило, огромная выгода.

Полезный тест: если вы даёте новому разработчику модуль billing, сможет ли он найти правила, доступ к данным, тесты и события биллинга в одном месте? Ему не должен требоваться прыжок по половине кода, чтобы понять один платёж клиента.

Держите домены раздельными, когда они меняются по разным причинам. Accounts могут меняться из‑за правил регистрации. Биллинг — из‑за счетов и налогов. Notifications — из‑за добавления SMS или новых шаблонов писем. Причины разные — модули тоже должны быть разными.

Не делайте модули крошечными только потому, что можно. Кодовая база с 30 микро‑модулями часто хуже, чем одна с восьмью понятными. Если два модуля всегда меняются вместе, у них один владелец и нет реальной бизнес‑границы — объедините их. Границы должны уменьшать трение, а не создавать церемонию.

Лучшие имена модулей обычно звучат скучно. Это хороший знак. Они соответствуют реальной работе компании, и это облегчает решения о релизах.

Установите строгие правила зависимостей

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

Выбирайте разрешённые зависимости осознанно и записывайте их простым языком. Billing может читать orders. Support может читать orders. Orders не должен импортировать support. Правила должны соответствовать бизнесу, а не случайности того, кто начал писать код раньше.

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

Самое безопасное правило и самое простое для объяснения: другие модули могут импортировать только из точки входа модуля. Им нельзя импортировать из внутренних папок. Если кто‑то тянет billing/internal/tax или support/private/macros, воспринимайте это как реальную ошибку.

Короткая настройка обычно достаточна:

  • Дайте каждому модулю один путь входа.
  • Держите внутренний код в явно приватных папках.
  • Добавьте автоматические проверки на запрещённые импорты.
  • Сломайте сборку или CI‑задачу при нарушении правила.

Последний шаг важнее, чем многие думают. Люди редко нарушают границы намеренно. Они делают это потому, что прямой импорт кажется быстрее, когда сроки поджимают. Если сборка блокирует этот путь, правило выживет в напряжённые недели, среди новых сотрудников и в срочных фикстурах.

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

Общий код без "ящика хлама"

Распутайте загруженный монолит
Получите помощь извне, чтобы распутать orders, billing, support и другие домены

Большинство кодовых баз получает папку common или utils рано или поздно. Часто она начинается небольшой, а затем превращается в мешалку для случайного кода, за которым никто не отвечает. Когда это происходит, границы быстро размываются.

Перемещайте код в общую область только тогда, когда он действительно разделяется. Хороший общий код решает одну и ту же низкоуровневую проблему для нескольких модулей и не несёт бизнес‑решений. Логирование, логика повторных попыток, парсинг ID и тестовые фикстуры часто подходят. Правила скидок, изменения статуса счёта и логика возвратов — нет. Это должно жить в billing или orders, даже если кто‑то ещё хотел бы похожую функциональность.

Полезный тест: общий код должен использоваться более чем одним модулем примерно одинаково. Он не должен содержать доменные термины, привязанные к одной команде. Его можно объяснить одним предложением. Если перенос меняет бизнес‑поведение — это не общий код.

Универсальные хелперы создают большинство проблем. utils.ts выглядит удобно, но приглашает несвязанный код в один ящик. Разделяйте хелперы по назначению. Держите вместе помощники времени, форматирования, транспортного кода и отдельные тестовые помощники. Это делает владение очевидным.

Бизнес‑правила должны иметь дом, и этот дом — модуль, который отвечает за результат. Если support нужно показать, можно ли вернуть заказ, support должен спросить orders или billing, а не копировать правила возврата в общий код только потому, что нужен тот же результат на одном экране.

Общий код требует уборки. Команды часто оборачивают вызовы библиотек, старые внутренние API или странные названия и забывают, почему обёртка существует. Пересматривайте общий код по расписанию. Удаляйте мёртвые обёртки, объединяйте дубли и возвращайте доменную логику в модули, если она туда съезжает.

Если общий модуль растёт быстрее продукта — это предупреждение. В чистой кодовой базе общий код остаётся маленьким, простым и легко объяснимым.

Как разделить существующую кодовую базу

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

Начните с бизнес‑областей, а не технических папок. Запишите, что продукт реально делает для пользователей: orders, billing, support, accounts, reporting или то, что подходит вашему приложению. Затем помапьте текущие файлы на эти области, даже если ответ грязный и один файл касается трёх областей.

Эта карта показывает боли. Выберите одну загруженную область сначала — обычно ту, до которой часто обращаются другие части кода. В многих приложениях это orders, потому что через него проходят многие потоки.

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

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

Команды часто начинают с простановки файлов в новые папки, но это меняет только форму. Реальная граница появляется, когда вызывающие теряют прямой доступ к внутренним деталям.

Когда вызывающие начинают пользоваться точками входа, добавьте проверки импортов в CI. Блокируйте импорты, которые пересекают границу в неправильном направлении. Это момент, когда граница перестаёт быть красивой диаграммой и начинает исполняться.

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

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

Простой пример: orders, billing и support

Очистите общий код
Вынесите бизнес‑правила из общих папок обратно в нужные модули

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

Модуль orders отвечает за покупку. Когда клиент нажимает "Купить", orders проверяет корзину, цены, скидки, правила запасов и варианты доставки. Если всё проходит, он создаёт заказ и сохраняет свои данные.

Billing имеет узкую задачу. Он снимает деньги, фиксирует результат оплаты и обрабатывает возвраты. Ему не нужно читать внутренности orders, чтобы понять, что делать. Orders должен посылать в billing только то, что billing реально нужно: ID заказа, сумму к списанию, ID клиента и токен способа оплаты.

Это правило важно. Если billing начнёт читать таблицы orders напрямую, он скоро начнёт зависеть от полей заказа, логики скидок и имён статусов. Тогда безобидное изменение в orders может сломать платежи.

Support тоже нужен доступ, но только через точку входа orders. Агент поддержки может спросить: "Отправлен ли этот заказ?" или "Был ли он отменён?" Support должен спрашивать orders о статусе, таймлайне или заметках, видимых клиенту. Он не должен патчить записи заказа сам и не должен угадывать статус по событиям billing.

Чистый поток выглядит так: orders создаёт покупку, billing снимает оплату, support читает состояние через публичные методы orders. Каждая область свою работу и остается в своей полосе.

Команда по‑прежнему деплоит одно приложение. Поэтому подход работает для многих малых команд: один процесс релиза, одна версия для тестирования и один путь отката при необходимости. Внутри этого деплоя границы остаются жёсткими.

Частые ошибки, которые ломают границы

Самый быстрый способ разрушить границы — позволить одному модулю лезть в таблицы другого. Сначала это кажется безобидным. Для изменения в orders нужен кусок данных billing — кто‑то пишет прямой запрос и уходит.

Через месяц billing поменяет схему — и orders сломается по причине, которую никто не ожидал. Граница на бумаге есть, но код её игнорирует. Если модулю нужно что‑то, он должен попросить через код владельца, а не читать или писать таблицы чужого модуля за спиной.

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

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

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

В этот момент common уже не общий код. Это второе приложение без владельца и границ. Если кусок кода принадлежит одному домену — держите его там, даже если другие модули вызывают похожую логику.

Тесты и скрипты тоже скрывают проблемы с зависимостями. Продакшен‑код может соблюдать границы, а фикстуры тестов импортируют что попало. Миграционные скрипты, админские скрипты и батчи делают то же самое. Это всё ещё связывает модули.

Быстрый ревью ловит большинство таких вещей. Ни один модуль не должен читать или писать таблицы другого напрямую. Ни один "временный" импорт не должен обходить правила. Общие пакеты — только маленькие технические хелперы, а не бизнес‑логика. Тесты и скрипты должны следовать тем же правилам зависимостей, что и код приложения. И список модулей должен быть достаточно коротким, чтобы у нового кода было очевидное место.

Слишком много модулей создаёт собственную путаницу. Если каждая мелочь становится отдельным модулем, люди перестают понимать структуру и начинают гадать, куда положить код. Эти догадки превращаются в новые утечки.

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

Быстрая проверка перед релизом

Устраните скрытые зависимости
Олег может замапить рискованные импорты и ужесточить правила зависимостей

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

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

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

Общий код должен оставаться небольшим и простым. Кладите туда только низкоуровневые хелперы, например парсинг дат или простое форматирование, но не бизнес‑правила.

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

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

Что делать дальше

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

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

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

Держите первое правило скучным и строгим. Простая проверка в CI лучше идеальной политики, о которой никто не помнит. Если сборка упадёт первый раз при нарушении, правило начнёт реально работать.

Код‑ревью не менее важно. Ревьюеры должны задавать один вопрос: "Сохраняет ли это изменение правильное направление зависимостей?" Эта привычка ловит много мусора до того, как он попадёт в основной код.

Если кодовая база уже годами переплетена, внешний обзор поможет сэкономить время. Oleg Sotnikov at oleg.is работает со стартапами и малыми командами как Fractional CTO, и такого рода чистка архитектуры часто — место, где начинаются спокойные релизы.

Цель скромна: одна чище граница, одно правило в сборке и одно правило в ревью. Сделайте это хорошо — и следующий релиз обычно пройдет намного спокойнее.

Часто задаваемые вопросы

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

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

На чём мне основывать модули?

Начните с бизнес‑задач: orders, billing, accounts, notifications и т. п. Не используйте папки вроде controllers или utils как основу — они описывают форму кода, а не ответственность.

Сколько модулей стоит создать сначала?

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

Как помешать людям импортировать приватный код из другого модуля?

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

Что реально должно быть в общем коде?

В общий код помещайте только низкоуровневые хелперы, которые одинаково используются разными модулями: логирование, логика повторных попыток, парсинг ID, тестовая поддержка. Бизнес‑правила оставляйте в модуле‑владельце результата.

Может ли один модуль читать таблицы БД другого модуля?

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

Как почистить запутанный код без переписывания?

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

Что модули должны раскрывать друг другу?

Экспонируйте только то, что действительно нужно другому модулю. Например, orders может посылать billing только ID заказа, сумму, ID клиента и токен способа оплаты; support может читать статус и таймлайн через API orders.

Что проверять перед каждым релизом?

Проверьте, что изменённые файлы по‑прежнему соответствуют своему модулю, импорты проходят через публичные точки входа, а общий код не содержит бизнес‑логики. Если правка требует обширного ручного тестирования во многих областях — приостановите релиз и исправьте утечку.

Как понять, что проблема с границами требует внешней помощи?

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