12 окт. 2025 г.·8 мин чтения

Маленькая архитектура программного обеспечения, которую ещё можно понять

Маленькая архитектура программного обеспечения помогает командам раньше замечать риски, сокращать расползание сервисов и лучше использовать AI, потому что один человек всё ещё может проследить всю систему.

Маленькая архитектура программного обеспечения, которую ещё можно понять

Почему это быстро усложняется

Система может казаться простой в понедельник и запутанной уже к пятнице. Изменение часто выглядит безобидно: один новый сервис для billing, ещё один для email, worker для фоновых задач, отдельное хранилище для поиска. Каждая часть решает свою локальную проблему. Вместе они делают продукт сложнее для понимания в целом.

Каждый новый сервис — это ещё одно место, которое нужно проверить, если что-то ломается. Вы читаете один лог, потом другой. Сравниваете timestamps. Пытаетесь понять, запрос упал, был повторён или попал в очередь, за которой никто не следит внимательно. Баг может быть мелким, но зона поиска очень быстро становится широкой.

Одно действие пользователя тоже может пересекать слишком много границ. Такой шаг, как «создать аккаунт», может затронуть API, базу пользователей, auth, email worker, analytics и настройку billing. Если ломается один переход, пользователь видит только «что-то пошло не так». Команде потом приходится распутывать всю цепочку, чтобы найти настоящую причину.

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

Через какое-то время никто уже не знает полный путь. Один разработчик знает API. Другой — worker. Кто-то ещё настроил очередь полгода назад и уже ушёл из компании. И вот уже никто не может простыми словами объяснить, что происходит между кликом пользователя и финальной записью в базу.

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

Как выглядит «достаточно маленькая» система

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

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

У каждого модуля должна быть одна понятная задача. Auth отвечает за вход. Billing — за планы и счета. Notifications — за email или сообщения. Как только модуль начинает лезть ещё в три области, дизайн уже становится мутным.

Данные тоже должны иметь одно место хранения. Если один и тот же статус клиента живёт в базе приложения, в cache, в reporting store и в отдельном сервисе со своей копией, люди перестают доверять системе. Маленькие системы держат источник истины простым и очевидным.

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

Отказы тоже должны выглядеть просто на бумаге. Если ломается база данных, запись останавливается. Если падает email worker, сообщения подождут, но основной продукт продолжит работать. Если вы не можете объяснить эти пути простыми словами, система уже слишком расползлась.

Именно поэтому Oleg Sotnikov часто подталкивает команды к более lean-системам. Когда форма остаётся компактной, люди раньше видят риск и исправляют его до того, как он превратится в ночной сеанс отладки.

Признаки, что вы разделили систему слишком рано

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

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

Та же проблема возникает, когда для одной функции нужны правки в нескольких репозиториях. Разработчик начинает в API, прыгает в worker, обновляет общие типы, потом чинит admin app. Теперь большая часть риска находится между репозиториями, а не внутри самой функции. Компактная система держит большинство изменений ближе к одному codebase или одному тесному модулю.

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

Плохие названия тоже выдают проблему. У команд появляются папки и сервисы с именами вроде «core», «shared», «platform» или «common», а потом на встречах спорят, где должен лежать код. Понятные границы обычно рождают простые названия, потому что задача и так очевидна.

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

Обычно шаблон видно и без долгого аудита. Одно и то же изменение каждую неделю попадает в несколько репозиториев. Одна пользовательская функция запускает несколько deploy. Shared libraries продолжают расти, потому что ownership размыт. Разработчики перестают запускать весь продукт локально. Названия сервисов нужно долго объяснять на встречах. Когда эти признаки появляются вместе, система шире, чем ей нужно.

Для многих SaaS-продуктов одного API, одного worker и понятных модулей внутри них достаточно надолго. Если разделение делает систему труднее объяснять, тестировать и менять, скорее всего, вы сделали его слишком рано.

Как уменьшить систему

Начните с тех путей, которыми пользователи пользуются каждый день: signup, payment, export отчёта, изменения настроек. Опишите каждый сценарий простыми словами, а затем отметьте, какие данные он читает и какие меняет. Очень часто одни и те же таблицы, jobs и правила встречаются в нескольких частях кода.

Эта карта покажет, где система стала шире, чем нужно. Если два сервиса трогают одни и те же данные, выходят в один и тот же срок и ломаются по одним и тем же причинам, часто это одна сущность, которая притворяется двумя. Объедините её. Отдельные репозитории и отдельные deploy быстро добавляют ментальную нагрузку.

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

Дублирующиеся правила — частый источник service sprawl. Проверки прав, лимиты тарифов, изменения статусов и логика цен часто повторяются в handlers, workers и admin tools. Вынесите эти правила в один модуль и держите его рядом с данными, которыми он владеет. Одно понятное место лучше пяти почти одинаковых версий.

Для большинства команд одного deployable unit хватает надолго. Держите отдельные модули внутри codebase, но выкатывайте их вместе, если только нагрузка или безопасность не заставляют разделять. Настоящая причина выглядит так: одной области нужна совсем другая масштабируемость, она работает с чувствительными данными и требует более строгой изоляции или обязана соблюдать жёсткую границу compliance. «Это может пригодиться потом» — не причина.

Маленькая архитектура на диаграмме часто выглядит менее впечатляюще. Но в production она работает лучше. Если billing, webhooks и изменения аккаунта всегда движутся вместе, держите их в одном приложении с тесными модулями. Вы сокращаете сетевые вызовы, рассинхрон конфигураций и ошибки при релизах. И что ещё важнее, один человек всё ещё может удержать всю форму в голове, а это по-прежнему один из лучших тестов здравого дизайна.

Простой пример из растущего продукта

Получите поддержку Fractional CTO
Работайте с Oleg над наведением порядка в архитектуре, внедрением AI и практичными инженерными решениями.

SaaS-стартап вышел на первые несколько тысяч пользователей и сделал то, что делают многие команды, когда рост ускоряется: разделил приложение на отдельные сервисы для billing, users, notifications и reports. Диаграмма выглядела чисто. Ежедневная работа стала сложнее.

Новая регистрация снаружи казалась простой. Человек создавал аккаунт, выбирал тариф, получал welcome email и ожидал увидеть первый отчёт позже. Но под капотом этот один сценарий теперь зависел от четырёх deploy, нескольких логов и пары очередей. Если одна часть замедлялась, сбой проявлялся далеко от настоящей причины.

Сервис billing мог списать деньги, а сервис users — не завершить создание аккаунта. Или аккаунт уже существовал, но service notifications пропускал welcome email после неудачного deploy. Ничего из этого не было драматичным. Это были мелкие, грязные сбои, которые делали дежурства шумными и трудными для понимания.

Команда откатила часть разделения. Notifications вернули в основное приложение, потому что email и in-app сообщения не нуждались в собственном цикле deploy, отдельном пуле worker и отдельном alerting. Уже одно это сняло неожиданно много накладных расходов.

Они всё ещё держали reports вне пути запроса, но перестали относиться к отчётности как к отдельному сервису. Report jobs работали за внутренней границей внутри основной системы. Приложение могло поставить задачу в очередь, один job runner обрабатывал её, а инженеры имели одно место, где можно было повторить сбой и посмотреть, что произошло.

После этого signup зависел от меньшего числа движущихся частей. Код на бумаге выглядел менее «чистым», но систему стало проще понимать. Нагрузка на дежурстве снизилась, потому что путь сбоя стал короче.

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

Почему AI-инструменты лучше работают на маленьких системах

AI-инструменты для программирования лучше всего работают, когда код помещается в одну ментальную картину. Один codebase с несколькими понятными модулями даёт модели достаточно контекста, чтобы вносить изменения, которые совпадают с остальной частью приложения. Разбейте тот же продукт на множество репозиториев и сервисов — и инструмент начнёт заполнять пробелы догадками.

Именно здесь и начинаются проблемы. Модель обновляет API handler, но пропускает worker, который использует тот же payload. Меняет одну schema и оставляет вторую копию нетронутой в другом репозитории. Пишет аккуратный тест на happy path, но не видит очередь, cache и background job, которые тоже влияют на результат.

Короткие цепочки вызовов помогают сильнее, чем кажется. Если запрос идёт от controller к business logic и дальше к базе данных, AI обычно справляется. Если тот же запрос прыгает через gateway, auth service, billing service, message bus и notification worker, полезного контекста быстро становится мало.

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

Названия здесь особенно важны. Модуль с названием billing/invoices даёт и людям, и AI понятную подсказку. Папка с названием common2 или helpers — нет. Чёткие границы также упрощают генерацию тестов, потому что инструмент видит, что относится к модулю, а что нужно замокать.

Человеческое review тоже становится лучше. Когда форма системы плотная, ревьюеры раньше замечают неверные правки, потому что всё ещё могут удержать поток в голове. Цель не в элегантности. Цель — сделать изменения проще для проверки до того, как они попадут в production.

Как маленькая архитектура показывает риск в production

Проверьте свои пути запросов
Найдите, где один пользовательский путь проходит слишком много сервисов, пока это не превратилось в шум инцидентов.

Когда запрос проходит через три части, сбой обычно можно проследить за несколько минут. Когда он прыгает через двенадцать сервисов, очередей, cache и webhooks, тот же баг превращается в охоту. Вы перестаёте спрашивать «что сломалось» и начинаете спрашивать «на каком переходе всё пошло не так».

Именно поэтому маленькая архитектура делает риск в production заметнее. Более простой путь запроса даёт короткий список мест, которые нужно проверить. Логи становятся понятнее, retries проще заметить, а один плохой timeout не прячется за пятью другими движущимися частями.

Checkout и signup особенно хорошо это показывают. В компактной системе приложение записывает заказ, списывает деньги с карты и отправляет подтверждение. Если что-то ломается, можно проследить запрос от приложения к базе и обратно. В расползшейся системе тот же поток может затронуть API gateway, auth, cart, billing, event bus, email worker и analytics. Одна медленная зависимость может заставить всё выглядеть случайным.

Понятное ownership не менее важно. У каждого важного факта должно быть одно место хранения. Если billing владеет статусом подписки, остальная часть системы должна читать его оттуда, а не хранить полукопии где-то ещё. Команды начинают путаться, когда несколько модулей считают, что владеют одним и тем же состоянием клиента. Тогда support видит один ответ, finance — другой, а инженеры тратят часы на то, чтобы решить, какому records доверять.

Rollback тоже становится проще. Если один deploy меняет одно приложение и одну схему базы данных, всё можно быстро откатить. Если пять сервисов должны оставаться на одинаковых версиях, rollback перестаёт быть страховкой и становится ещё одним инцидентом.

Шум от alert тоже снижается по той же причине. Чем меньше сервисов общаются друг с другом, тем меньше ошибок соединения, меньше retry-storms и меньше дублирующихся уведомлений об одной и той же проблеме. Маленькая система всё ещё ломается, но ломается так, чтобы люди могли это понять.

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

Ошибки, из-за которых система остаётся слишком большой

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

Одна из частых ошибок — делить по tech stack, а не по пользовательскому сценарию. «Python-сервис делает AI, Node-сервис делает API, Go-сервис обрабатывает фоновую работу» на диаграмме выглядит аккуратно. На практике одна функция теперь проходит через три репозитория, три deploy и три места, где можно упасть. Маленькие системы работают лучше, когда одно действие пользователя соответствует небольшому и понятному куску кода.

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

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

Есть простой тест. Спросите: «Если мы уберём этот сервис на следующей неделе, какая проблема сразу вернётся?» Если никто не может ответить одним предложением, сервис, скорее всего, просто тянет систему вниз.

Shared libraries тоже создают скрытый раздув. Они выглядят аккуратно, потому что код лежит в одном месте, но часто размывают ownership. Тогда одно маленькое изменение требует согласования сразу нескольких людей, и никто не хочет трогать этот код. Более тесный модуль внутри основного приложения часто менять, тестировать и понимать проще.

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

Быстрая проверка перед добавлением ещё одного сервиса

Сделайте AI-разработку полезнее
Меньшие системы дают AI-инструментам лучший контекст и вашей команде — меньше слепых зон.

Новый сервис выглядит аккуратно на диаграмме. В production он означает ещё один deploy, ещё один набор логов и ещё одно место, где данные могут разойтись или сломаться. Разделение должно решать реальную проблему сейчас, а не смутный страх перед будущим.

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

Прежде чем выносить что-то отдельно, задайте несколько прямых вопросов. Может ли один модуль внутри текущего приложения решить это чисто, если ужесточить границу? Будет ли новый сервис владеть данными, которые другим частям системы не следует читать или писать напрямую? Может ли один человек описать полный путь сбоя меньше чем за минуту — от действия пользователя до плохого результата? Можно ли тестировать и выкатывать его тем же pipeline, который команда уже использует, без лишних ручных шагов? Убирает ли разделение измеряемое узкое место, например масштабирование, security или проблемы с uptime, а не предполагаемое?

Если на большую часть вопросов ответ «нет», оставьте код на месте. Лишние сервисы часто скрывают слабые границы вместо того, чтобы исправлять их. Они ещё и создают тихие расходы: больше alerts, больше config, больше secrets и больше способов, которыми staging и production начинают расходиться.

Простой пример помогает. Если растущий продукт начинает тяжело обрабатывать видео и эта работа блокирует web requests, отдельный worker может иметь смысл. У него свой runtime, queue и свои требования к масштабированию. Если же команда хочет просто «более чистый код», обычно достаточно хорошо оформленного модуля внутри основного приложения.

Эта склонность к меньшему числу движущихся частей экономит время позже. Когда что-то ломается в 2 часа ночи, команда может быстро проследить путь и сначала исправить именно то, что нужно.

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

На этой неделе выберите один реальный пользовательский сценарий и проследите его end to end. Возьмите обычный путь, а не редкий admin route. Signup, создание первого проекта или checkout — хорошие варианты. Запишите каждый сервис, очередь и базу данных, которых касается этот поток.

Этот подсчёт говорит больше, чем диаграмма. Если одно простое действие прыгает через шесть или семь движущихся частей, у вас больше production risk, чем нужно. Это ещё и показывает, где AI-инструменты для программирования будут буксовать, потому что логика живёт сразу в слишком многих местах.

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

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

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

После этого запишите правила ownership простыми словами. Сделайте их настолько короткими, чтобы новый инженер мог прочитать их за две минуты. «Приложение владеет signup» и «Только worker отправляет email» лучше, чем абстрактные правила, которые никто не помнит.

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

Если вам нужен второй взгляд, Oleg Sotnikov на oleg.is работает со стартапами как Fractional CTO над наведением порядка в архитектуре и практичным внедрением AI. Такой внешний review особенно полезен, когда продукт ещё кажется управляемым, а service sprawl ещё не стал нормой.

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

Что значит «достаточно маленькая» архитектура программного обеспечения?

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

Когда стоит выносить код в новый сервис?

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

Как понять, что мы разделили систему слишком рано?

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

Очереди помогают ускорить медленные запросы?

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

Как уменьшить архитектуру без полной переписывания?

Начните с одного частого пользовательского сценария, например signup или checkout, и опишите все чтения, записи и побочные эффекты простыми словами. Потом объедините части, которые всегда меняются вместе, вынесите повторяющиеся бизнес-правила в один модуль и держите данные рядом с кодом, который ими владеет.

Почему AI-инструменты лучше работают на маленьких системах?

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

Сколько сервисов обычно нужно раннему SaaS-продукту?

Для многих SaaS-продуктов одного API, одного worker и понятных модулей внутри каждого хватает гораздо дольше, чем ожидает команда. На этом уровне можно оставаться, пока traffic, security или uptime не потребуют настоящего разделения.

Какой пользовательский сценарий стоит проверить первым?

Сначала проследите обычный, часто используемый путь, а не крайний случай. Signup, создание первого проекта, payment или export отчёта покажут реальный путь запроса, владельцев данных и места, где прячутся сбои.

Как понять, что сервис всё ещё нужен?

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

Когда стоит просить внешнюю помощь по архитектуре?

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