Новый веб-продукт ещё не запущен.
Пользователей пока нет.
Нагрузка неизвестна.
Бизнес-модель ещё несколько раз изменится.
Но на архитектурном обсуждении уже появляется вопрос:
А будем сразу делать микросервисы?
Аргументы обычно звучат убедительно:
Проект ведь будет расти.
Потом монолит станет сложно поддерживать.
Микросервисы лучше масштабируются.
Большие компании работают именно так.
В результате приложение, у которого пока есть пять экранов и один разработчик, может ещё до первого клиента получить:
API Gateway
│
├── Auth Service
├── User Service
├── Project Service
├── Notification Service
├── File Service
└── Billing ServiceУ каждого сервиса:
Docker;
environment;
порт;
API;
deployment;
логи;
health-check;
конфигурация.Вместо одного приложения команда получает распределённую систему.
Но бизнес-функций от этого больше не становится.
Поэтому при проектировании нового продукта мы бы начинали не с вопроса:
«Монолит или микросервисы?»
А с другого:
«Какая архитектурная сложность действительно нужна продукту сегодня — и как оставить возможность разделить его завтра?»
Для большинства новых CRM, SaaS, клиентских кабинетов, внутренних систем и B2B-сервисов между двумя крайностями есть особенно интересный вариант:
модульный монолит.
Именно с него начнём.
Что такое монолит на практике
Слово «монолит» часто используется почти как ругательство.
Будто монолит обязательно выглядит так:
app.js
18 000 строк
if (...)
if (...)
if (...)Это неверно.
Монолит означает прежде всего, что система разворачивается как единое приложение или тесно связанный deployment unit.
Например:
Internet
↓
Nginx
↓
Application
↓
PostgreSQLВнутри приложения могут находиться:
авторизация;
клиенты;
проекты;
файлы;
уведомления;
платежи;
админ-панель.Но все они собираются и разворачиваются вместе.
Это само по себе не является архитектурной проблемой.
Проблема начинается, когда внутри приложения отсутствуют границы.
Плохой монолит и хороший монолит — разные вещи
Плохая структура:
routes/
services/
helpers/
utils/
models/где любой файл импортирует любой другой.
Например:
payment-serviceнапрямую изменяет таблицы проектов.
notification-service самостоятельно читает внутренние таблицы платежей.
Модуль пользователей знает детали файлового storage.
А удаление одного поля требует правок в пятнадцати местах.
Это уже не столько проблема монолитного deployment.
Это проблема связности кода.
Модульный монолит устроен иначе
Представим CRM:
Application
│
├── Identity
├── Customers
├── Projects
├── Billing
├── Files
└── NotificationsКаждый модуль отвечает за собственную часть бизнеса.
Например:
Billingзнает:
Invoices
Payments
Refundsа:
Projectsзнает:
Project
Stage
ChangeRequestОдин модуль не должен произвольно менять внутренние данные другого.
Внешне это всё ещё одно приложение
Например:
Browser
↓
Nginx
↓
Node.js Application
│
├── Identity
├── Customers
├── Projects
├── Billing
├── Files
└── Notifications
↓
PostgreSQLУ нас:
один deployment;
один runtime;
одна система логов;
одна основная база;
один release.Но внутри уже существуют границы.
И это принципиально важно.
Хорошие границы позволяют разделить систему позже
Сегодня:
Application
│
├── Billing
├── Files
└── ProjectsЧерез два года может выясниться, что Billing развивается независимо.
Тогда:
Application
│
├── Projects
└── Files
Billing ServiceЕсли модуль изначально имел понятный контракт, извлечение становится значительно проще.
Если же Billing на протяжении двух лет напрямую обращался ко всем таблицам проекта, разделение превращается в хирургическую операцию по всему приложению.
Поэтому архитектурный вопрос на старте звучит так
Не:
Сколько микросервисов нам создать?
А:
Какие бизнес-границы уже сейчас стоит сделать явными?
Это намного полезнее.
Разберём реальный новый SaaS
Допустим мы создаём B2B-сервис управления проектами.
Функции первой версии:
регистрация;
организации;
роли;
проекты;
сообщения;
файлы;
уведомления;
оплата.На старте:
100–500 пользователей;
одна команда;
одна production-инфраструктура.Самый простой монолит:
Node.js
+
PostgreSQLспокойно способен обслуживать такой продукт.
Но мы всё равно можем разделить domain на модули:
identity/
organizations/
projects/
messages/
files/
billing/
notifications/Получается модульный монолит.
Зачем делать модули, если deployment всё равно один
Потому что главная архитектурная сложность большого продукта находится не только в количестве серверов.
Она находится в количестве зависимостей между его частями.
Например правило:
Billing
не изменяет Project напрямую.Вместо:
await db.projects.update(...);он вызывает публичную операцию:
Projects.activateAfterPayment(...)или публикует внутреннее событие:
PAYMENT_CONFIRMEDТеперь граница уже существует.
Физически процессы пока не разделены.
Логически — разделены.
Внутренний вызов значительно дешевле сетевого
В модульном монолите:
Projects
↓
Billingможет быть обычным вызовом функции.
Если превратить Billing в отдельный сервис:
Projects
↓
HTTP
↓
Billing Serviceпоявляется целый новый класс проблем.
Например:
timeout;
DNS;
network failure;
retry;
authentication;
API version;
serialization;
latency.Раньше вызов либо произошёл, либо выбросил exception внутри одного процесса.
Теперь между двумя строками бизнес-логики находится сеть.
Сеть — одна из главных цен микросервисов
Представим монолит:
const customer =
await customers.get(id);В случае микросервисов это может превратиться в:
Projects Service
↓
HTTP request
↓
Customers Service
↓
PostgreSQL
↓
HTTP responseЧто если Customers Service:
работает медленно?Что если request дошёл, но response потерялся?
Что если сервис перезапускается?
Что если первая попытка выполнила mutation, а client сделал retry?
Мы снова приходим к:
timeouts;
retries;
idempotency;
circuit breakers.И это уже не теория.
Это обязательная часть distributed architecture.
В монолите одна транзакция может охватить несколько модулей
Представим оформление заказа.
Нужно:
создать Order;
уменьшить Stock;
создать PaymentAttempt.В одном PostgreSQL это может быть:
BEGIN
INSERT order
UPDATE stock
INSERT payment_attempt
COMMITЕсли что-то не получилось:
ROLLBACKСистема возвращается к исходному состоянию.
В микросервисах такой роскоши может уже не быть
Представим:
Order Service
Inventory Service
Payment ServiceУ каждого собственная БД.
Теперь операция:
Создать заказвыглядит:
Order Service
↓
Inventory Service
↓
Payment ServiceПервая операция прошла.
Вторая прошла.
Третья упала.
Что делать?
Обычный:
ROLLBACKчерез три независимые базы уже не работает.
Появляется distributed consistency
Нужно проектировать:
events;
compensating actions;
outbox;
idempotency;
saga.Например:
ORDER_CREATED
↓
RESERVE_STOCK
↓
STOCK_RESERVED
↓
CREATE_PAYMENTЕсли платёж создать не удалось:
PAYMENT_FAILED
↓
RELEASE_STOCKМы заменили одну database transaction на отдельный распределённый business workflow.
Иногда это оправдано.
Но бесплатно такое разделение не бывает.
Поэтому микросервисы — не способ упростить маленький продукт
Они переносят сложность.
Монолит имеет сложность:
внутри одного codebase.Микросервисы:
между системами.Вместо одного приложения нужно управлять взаимодействием нескольких независимых приложений.
Что действительно дают микросервисы
Когда система уже достаточно большая, преимущества становятся реальными.
Первое — независимый deployment.
Например:
Billingизменяется десять раз в месяц.
А:
Document Archiveраз в полгода.
В монолите каждое изменение Billing требует release всего приложения.
В микросервисной архитектуре:
Billing v18можно обновить отдельно.
Но это работает только если сервис действительно независим.
Если для deployment одного сервиса нужно одновременно обновить пять других — это не настоящая независимость
Например:
Service A v4
requires
Service B v7
requires
Service C v12И deployment всегда выполняется:
A + B + CМы получили микросервисы физически.
Но монолитный release логически остался.
Это один из признаков distributed monolith.
Distributed monolith сочетает недостатки двух подходов
У него уже есть:
сеть;
deployment нескольких сервисов;
distributed logs;
timeouts;
API versioning.Но нет главного преимущества:
независимости.Получается:
сложность микросервисов
+
связность монолита.Такой результат особенно часто появляется, когда систему разделили по техническим слоям.
Например плохое разделение
User Database Service
Project Database Service
Email Service
Validation ServiceКаждая простая операция начинает делать:
HTTP
↓
HTTP
↓
HTTP
↓
HTTPСервисы слишком мелкие и постоянно разговаривают друг с другом.
Размер микросервиса определяется не количеством строк
Нет правила:
5000 строк
→ пора выносить.Или:
50 таблиц
→ микросервис.Гораздо важнее понятие business capability.
Например:
Billingможет быть самостоятельной способностью бизнеса.
Он имеет:
счета;
платежи;
возвраты;
тарифы;
правила.И потенциально отдельный lifecycle.
А:
FormattingServiceвряд ли является отдельным business domain только потому, что содержит много кода.
Второе преимущество — независимое масштабирование
Представим SaaS.
Обычный API:
CPU 15%Но обработка изображений:
CPU 100%В монолите приходится масштабировать всё приложение:
Application × 5хотя дополнительная мощность нужна только одному типу задач.
При разделении можно получить
Core API × 2
Image Processing × 10Теперь инфраструктура масштабируется под реальную нагрузку.
Это хороший аргумент в пользу выделения отдельного сервиса.
Но «у нас будет высокая нагрузка» недостаточно
Нужно знать:
Где именно она будет?
Если все части продукта растут примерно одинаково, обычное горизонтальное масштабирование монолита может оказаться вполне разумным.
Например:
Nginx
│
├── API #1
├── API #2
├── API #3
└── API #4
↓
PostgreSQLСам факт:
много пользователейне требует автоматически микросервисов.
Третье преимущество — fault isolation
Представим отдельно работающий:
Report GeneratorОн получает тяжёлый файл и падает из-за memory limit.
Если он находится внутри основного API:
API process
↓
OOM
↓
основной сервис упал.Если обработчик изолирован:
Report Worker
↓
OOMосновной продукт продолжает работать.
Это уже реальная operational выгода.
Поэтому тяжёлая обработка часто становится хорошим первым кандидатом на разделение
Например:
video conversion;
AI processing;
large PDF generation;
image processing;
document recognition.У таких задач:
другая нагрузка;
другой runtime;
другие требования к памяти;
часто асинхронный lifecycle.Их изоляция может быть оправдана раньше, чем разделение обычного CRUD.
Но отдельный worker ещё не обязательно микросервис
Например:
API
Worker
PostgreSQLмогут оставаться одним приложением и одним codebase.
Worker просто имеет другой entrypoint.
Это важный момент.
Не каждое разделение процессов означает микросервисную архитектуру.
Четвёртое преимущество — разные технологические требования
Допустим основной backend написан на Node.js.
Но для ML-обработки удобнее:
Python.Нет необходимости переписывать весь backend.
Можно получить:
Core API
Node.js
AI Processing
Pythonмежду которыми существует контролируемый контракт.
Это оправданное технологическое различие.
Но «хотим попробовать другой язык» — слабая причина
Если каждый разработчик выбирает:
Node.js
Go
Python
Rust
Javaдля маленьких сервисов только из интереса, эксплуатационная стоимость резко растёт.
Теперь нужно поддерживать:
пять toolchain;
пять наборов библиотек;
пять способов сборки;
пять типов runtime.Polyglot architecture полезна, когда технология решает конкретную проблему.
Не сама по себе.
Пятое преимущество — независимые команды
Это одна из самых важных причин для зрелой организации.
Представим продукт разрабатывают:
3 человека.Создание:
12 микросервисовне создаёт 12 независимых команд.
Все сервисы всё равно меняют одни и те же люди.
А теперь представим 80 разработчиков
Есть команды:
Billing Team
Identity Team
Marketplace Team
Fulfillment TeamКаждой трудно координировать release общего огромного приложения.
Теперь самостоятельные сервисы начинают уменьшать организационную связанность.
Архитектурные границы совпадают с границами ответственности команд.
Вот здесь микросервисная модель раскрывает гораздо больше пользы.
Один разработчик не получает автономию от самого себя
Поэтому размер команды — реальный архитектурный фактор.
Микросервисы особенно полезны, когда появляется необходимость:
нескольким командам
независимо
разрабатывать
тестировать
разворачивать
свои части продукта.Если команда одна, значительная часть этого преимущества исчезает.
Шестое преимущество — разный release cadence
Например:
Core accounting:
изменяется редко,
очень осторожно.А:
Recommendation Service:
эксперименты каждую неделю.Разделение позволяет экспериментировать с одной частью, не затрагивая другую.
Это особенно полезно, если у разных частей системы действительно разная скорость изменений.
Но микросервис должен владеть своим поведением
Представим:
Billing Serviceно его таблицы напрямую читает:
Projects Service
Analytics Service
Admin ServiceТеперь Billing не может самостоятельно изменить схему.
Любая migration требует координации со всеми потребителями.
Независимость потеряна.
Поэтому данные становятся одним из самых сложных вопросов
Настоящая микросервисная автономность обычно предполагает, что сервис владеет своими domain data.
Например:
Billing
↓
Billing DBДругие сервисы получают сведения через:
APIили:
events.Они не делают:
SELECT *
FROM billing.payments;напрямую.
Почему одна общая база для всех сервисов опасна
Допустим есть:
User Service
Project Service
Billing Serviceно все используют:
shared PostgreSQLи свободно читают таблицы друг друга.
На схеме это микросервисы.
На уровне данных — один связанный монолит.
Например Billing меняет:
payments.statusи неожиданно ломает Project Service, который читал это поле напрямую.
Значит ли это, что каждому сервису обязательно нужен отдельный PostgreSQL-сервер?
Нет.
Data ownership — логическая граница.
На небольшом этапе можно физически использовать один кластер PostgreSQL, но разделить:
schema;
credentials;
ownership.Например:
billing.*
projects.*
identity.*и не разрешать произвольный доступ между областями.
По мере роста физическое разделение можно выполнить позднее.
Микросервисы меняют способ построения отчётов
В монолите запрос:
SELECT ...
FROM projects
JOIN payments
JOIN usersестественен.
После разделения:
Projects DB
Payments DB
Users DBтакого JOIN больше нет.
Что делать?
Появляются новые архитектурные варианты
Например:
Analytics projectionполучает события:
PROJECT_CREATED
PAYMENT_CONFIRMED
USER_REGISTEREDи строит собственную read model.
Или отдельный reporting pipeline.
Опять же мы получили дополнительную инфраструктуру только потому, что разрезали данные.
Это не плохо
Для крупной системы это может быть именно правильным решением.
Но важно понимать причинно-следственную связь:
микросервисная автономия покупается дополнительной распределённой сложностью.
Локальная разработка тоже становится сложнее
Монолит:
docker compose upподнимает:
App
PostgreSQL
Redisи разработчик начинает работу.
Микросервисная система может потребовать
Gateway
Identity
Users
Projects
Billing
Notifications
Files
Kafka/RabbitMQ
PostgreSQL × N
Redis
Object StorageДля изменения одной кнопки разработчику внезапно требуется половина production-архитектуры на ноутбуке.
Поэтому нужен серьёзный local development story
Например:
Docker Compose;
development mocks;
service contracts;
seed data;
local message broker.Чем больше сервисов, тем важнее инструменты разработчика.
Иначе скорость команды вместо роста уменьшается.
Тестирование тоже становится другим
Монолит позволяет относительно просто запустить:
application
+
test databaseи провести integration test.
В микросервисах появляются:
service A;
service B;
service C;
contracts;
events;
network.Теперь нужно решить:
что мокировать;
что поднимать реально;
как проверять compatibility.Возникают contract tests
Например Projects ожидает от Billing:
{
"status": "paid",
"amount": 50000
}Billing обновился и начал возвращать:
{
"paymentStatus": "paid"
}Оба сервиса по отдельности:
tests PASS.Вместе:
production FAIL.Контракт становится самостоятельной частью архитектуры.
API versioning тоже перестаёт быть только публичной проблемой
Даже внутренние сервисы должны менять контракты аккуратно.
Например:
v1ещё используется старым Project Service.
Новый Billing уже знает:
v2.Нужно обеспечить период совместимости или координировать rollout.
Внутри монолита compiler или test suite иногда обнаружил бы изменение сразу.
Через сеть ошибки проявляются иначе.
Наблюдаемость микросервисов значительно сложнее
В монолите пользовательский запрос:
POST /checkoutможно найти в одном application log.
В микросервисах:
Gateway
↓
Order
↓
Inventory
↓
Payment
↓
NotificationОшибка появляется:
на четвёртом шаге.Теперь нужны:
correlation ID;
distributed tracing;
centralized logs;
metrics.Иначе расследование выглядит:
Давайте откроем логи пяти контейнеров и попробуем сопоставить время.
Один request ID становится особенно ценным
Например:
requestId:
req-81a7проходит через:
Gateway
Order Service
Payment Service
Notification ServiceТеперь можно восстановить маршрут одного пользовательского действия.
Без этого микросервисы резко усложняют поддержку.
Monitoring тоже размножается
Монолит:
API
Database
WorkerМикросервисы:
Service A health
Service B health
Service C health
...Нужно понимать:
какой сервис критичен;
какой degraded;
куда направлять alert;
какая команда отвечает.Само разделение кода не решает эту задачу.
Security perimeter тоже расширяется
В монолите:
Browser
↓
APIОсновная authorization выполняется внутри приложения.
В микросервисах появляются:
service-to-service authentication;network policies;internal secrets;service identities.Нужно решить:
Может ли Projects Service доверять запросу от Billing?
Может ли произвольный контейнер вызвать admin endpoint Identity Service?
Количество внутренних trust boundaries увеличивается.
Поэтому «микросервис находится внутри нашей сети» недостаточно
Внутренняя сеть не должна автоматически означать:
полный доступ ко всему.Для зрелой системы service identity становится частью безопасности.
Это дополнительная operational стоимость, которую монолиту в таком виде оплачивать не приходится.
Когда монолит действительно становится проблемой
Теперь перейдём к противоположной крайности.
Оставлять систему монолитной навсегда тоже не является универсальной целью.
Есть реальные признаки, что отдельную область пора выделять.
Первый признак — части продукта масштабируются совершенно по-разному
Например:
Core API:
2 instancesVideo Processing:
30 workersЕсли ради video processing приходится масштабировать весь core application, граница очевидна.
Второй признак — независимые команды постоянно мешают друг другу
Например:
Billing Teamи:
Marketplace Teamработают независимо, но каждый release требует общей координации.
Разделение может уменьшить организационный bottleneck.
Третий признак — компонент имеет явно собственный lifecycle
Например платёжный контур:
Payment
Refund
Invoice
Subscription
Webhookстал достаточно самостоятельным domain.
У него:
отдельные бизнес-правила;
отдельная команда;
отдельные releases;
отдельные риски.Это гораздо более сильный аргумент, чем:
Файл billing.js стал большим.
Четвёртый признак — требуется fault isolation
Тяжёлый модуль:
PDF generationрегулярно оказывает влияние на основной API.
Если архитектурная изоляция уменьшает blast radius, выделение приносит measurable пользу.
Пятый признак — технология или инфраструктура принципиально отличается
Например:
Core:
Node.js + PostgreSQLно:
Search:
Elasticsearchили:
ML:
Python + GPU.Отдельный runtime начинает быть естественным.
Шестой признак — нужна независимая частота deployment
Если небольшой модуль выпускается:
20 раз в неделю,а основной продукт:
раз в месяц,единый release может становиться искусственным ограничением.
Седьмой признак — граница domain уже хорошо понятна
Это особенно важно.
Разбить систему легко.
Правильно выбрать границы — сложно.
Если продукт только создаётся, мы ещё можем не знать:
Billing и Subscription — один domain или два?
Project и Task должны жить вместе?
Notification относится к бизнес-модулю или является инфраструктурой?
По мере эксплуатации реальные границы становятся намного заметнее.
Это одна из причин не спешить
Если создать микросервисы слишком рано, они закрепят предполагаемые границы.
Потом бизнес-модель изменится.
И выяснится:
Service Aдолжен постоянно синхронно обращаться к:
Service B.Мы физически разрезали то, что логически должно было остаться вместе.
Исправлять неправильные service boundaries дорого
Внутри монолита перенос:
функции из модуля A в Bможет быть обычным refactoring.
Между микросервисами это может потребовать:
перенос API;
данных;
events;
deployment;
monitoring;
permissions.Поэтому стабильность domain boundaries имеет большую ценность.
Martin Fowler ещё много лет назад сформулировал подход Monolith First
Суть идеи очень практична:
сначала продукт развивается достаточно компактно, чтобы команда могла понять реальные границы domain, а микросервисы выделяются тогда, когда сложность монолита и организационные требования действительно начинают это оправдывать.
Это не означает:
Микросервисы плохие.
Это означает:
Их стоимость должна решать существующую проблему.
Какие причины мы не считали бы достаточными
Например:
«Так современнее».Слабая причина.
«Потом будет миллион пользователей».Недостаточно без понимания нагрузки.
«У Netflix микросервисы».Размер Netflix ничего не говорит о вашем стартапе.
«Файл стал большим».Можно разбить модуль внутри приложения.
«Хотим Kubernetes».Infrastructure tool не должен определять domain architecture.
Особенно опасный аргумент — «сразу сделаем правильно»
Микросервисная архитектура не является финальной «правильной» стадией любой системы.
Она является одним из способов организации системы с конкретными преимуществами и конкретной ценой.
Для некоторых продуктов лучший результат на протяжении всей жизни может выглядеть:
Nginx
↓
Modular Monolith × 2
↓
PostgreSQL
↓
WorkersИ это абсолютно нормальная production-архитектура.
Миллион пользователей сам по себе тоже не требует микросервисов
Представим продукт с огромным количеством:
GETи редкими writes.
Хорошо оптимизированный монолит можно масштабировать горизонтально:
Load Balancer
│
├── App #1
├── App #2
├── App #3
└── App #4с отдельным PostgreSQL, Redis и CDN.
Архитектура остаётся понятной.
Монолит не означает один сервер
Это распространённое заблуждение.
Монолит может иметь:
20 application instances.Монолитность относится к architecture/deployment unit, а не к количеству VPS.
Так же как микросервисы можно неудачно запустить:
все на одном VPS.Количество машин и форма кода — разные измерения.
Вертикальное и горизонтальное масштабирование существуют до микросервисов
Сначала можно:
увеличить CPU/RAM;далее:
добавить application replicas;далее:
вынести files в S3;добавить Redis;вынести workers.И лишь затем, если появляются реальные границы, разделять domain services.
Рост продукта не обязан выглядеть как один большой прыжок:
монолит
↓
50 микросервисов.Практичнее эволюционная архитектура
Например новая CRM.
Этап 1
Nginx
↓
Modular Application
↓
PostgreSQLЭтап 2
Появляются фоновые задачи:
API
Worker
PostgreSQL
RedisCodebase всё ещё может быть одним.
Этап 3
Файлов стало много:
Object StorageЭтап 4
AI-processing требует GPU:
AI Processing ServiceЭтап 5
Billing обслуживает несколько продуктов:
Billing ServiceСистема разделяется там, где есть причина.
Это значительно отличается от проектирования микросервисов по списку таблиц
Плохая схема:
users
→ User Service
projects
→ Project Service
messages
→ Message Service
files
→ File ServiceТаблица не является автоматически domain boundary.
Например:
Project
Task
Stage
ChangeRequestмогут составлять один цельный domain.
Разделять их только потому, что таблицы разные, бессмысленно.
Микросервисы полезнее строить вокруг бизнес-возможностей
Например:
Order Managementили:
Billingили:
Identityа не:
TableService.Тогда сервис способен отвечать за завершённую часть поведения.
И не каждый модуль должен когда-нибудь стать микросервисом
Это тоже важно.
Например модуль:
User Preferencesможет спокойно жить внутри основного приложения десять лет.
Модульная архитектура не является:
списком будущих микросервисов.
Это способ держать систему понятной независимо от способа deployment.
Как подготовить монолит к возможному разделению
Мы бы начали с нескольких правил.
Во-первых, каждый domain module имеет понятный публичный интерфейс.
Например:
Billing.confirmPayment()
Billing.createInvoice()
Billing.refund()а не прямой доступ ко всем внутренним таблицам.
Во-вторых, ограничиваем cross-module data access
Например Projects не должен знать структуру:
billing_refundsЕму нужен бизнес-факт:
payment confirmed.Это позволит позже изменить внутреннюю модель Billing без переписывания Projects.
В-третьих, полезны domain events
Например:
PROJECT_COMPLETEDинтересует:
Notifications;
Analytics;
Billing.В монолите event может распространяться через простой внутренний event bus.
Позже тот же контракт можно перенести на внешнюю очередь.
То есть не обязательно начинать с Kafka
Сегодня:
in-process eventЗавтра:
durable PostgreSQL jobПозже:
message broker.Бизнес-событие остаётся концептуально тем же.
Infrastructure развивается постепенно.
В-четвёртых, стоит избегать circular dependencies
Плохо:
Projects
→ Billing
→ Projects
→ Notifications
→ BillingТакой graph очень трудно разделить.
Лучше понимать направление dependencies и переносить shared concepts на подходящий уровень.
В-пятых, модуль должен иметь владельца данных
Даже внутри одной БД можно договориться:
projects tables
→ Projects modulepayments tables
→ Billing moduleДругой модуль не выполняет произвольные UPDATE.
Это дисциплина, которая ничего не стоит на уровне infrastructure, но сильно облегчает будущее разделение.
Как понять, какой сервис выносить первым
Не нужно выбирать самый большой модуль.
Хороший кандидат обычно имеет сразу несколько свойств:
понятная domain boundary;мало тесных транзакций с остальными;самостоятельный workload;реальная причина независимого deployment;измеримая выгода от isolation.Пример: генерация документов
Допустим SaaS создаёт большие PDF.
Первоначально:
Core API
↓
generatePDF()При большом документе:
CPU 100%
RAM 1.5 GBAPI начинает тормозить.
Хороший следующий шаг:
Core API
↓
Job Queue
↓
Document WorkerЕсли позже subsystem развивается отдельно:
Document ServiceОн уже имеет естественную границу.
Пример: Notifications
На старте:
sendEmail()внутри приложения.
Потом появляются:
email;
Telegram;
SMS;
push;
templates;
retries;
delivery status;
preferences.И нагрузка растёт.
Теперь Notifications начинает выглядеть как самостоятельная capability.
Можно выделять.
Но выносить его в первый день необязательно
Если продукт отправляет:
20 писем в день,отдельный Notification Service вряд ли решит реальную проблему.
Он просто создаст дополнительный deployment.
Пример: Billing
На старте:
один тариф;
один способ оплаты;
один webhook.Модуль внутри монолита может быть достаточен.
Через два года:
subscriptions;
multiple providers;
refunds;
invoices;
tax;
plans;
limits;
entitlements.Billing становится самостоятельной сложной подсистемой.
Теперь выделение может иметь хороший смысл.
Но выносить Billing нужно осторожно из-за данных
Другие части продукта не должны одновременно продолжить:
UPDATE paymentsнапрямую.
Иначе сервис физически вынесен, а ownership не изменился.
Правильная миграция архитектуры включает и изменение зависимостей.
Как технически извлекать сервис из модульного монолита
Обычно безопаснее делать это постепенно.
Было:
Core
├── Projects
└── BillingСначала вводим явный internal interface:
Projects
↓
Billing API abstractionХотя Billing ещё внутри того же процесса.
Затем убираем прямой доступ к данным
Projects больше не делает:
SELECT *
FROM payments;Только:
Billing.getPaymentStatus(...)Следующим этапом можно отделить данные
Например:
billing schemaполучает отдельного DB user.
Другие модули больше не имеют прямых прав.
Затем implementation можно вынести за сеть
Было:
Billing.getStatus()Стало:
HTTP/RPC
↓
Billing ServiceCalling code концептуально уже готов к границе.
Потом появляется независимый deployment
Core v18
Billing v6И только тогда можно говорить, что сервис действительно стал автономнее.
Такой путь намного безопаснее большого переписывания
Плохая стратегия:
Сейчас за три месяца перепишем весь монолит на 30 микросервисов.
До завершения проекта:
старый продукт продолжает меняться;новая архитектура отстаёт;границы оказываются неправильными.Поэтапное извлечение позволяет получать пользу после каждого этапа.
Иногда выделение сервиса можно остановить посередине
Например после:
ясных module boundaries
+
отдельного workerпроблема исчезла.
Нет никакой обязанности продолжать:
↓
HTTP service
↓
отдельная DB
↓
Kubernetes.Архитектура существует для продукта.
Не продукт для архитектуры.
Сравним варианты
| Критерий | Монолит | Модульный монолит | Микросервисы |
|---|---|---|---|
| Запуск нового продукта | Очень простой | Простой | Сложнее |
| Deployment | Один | Один | Несколько независимых |
| Локальная разработка | Простая | Простая | Сложнее |
| Транзакции | Простые | Простые | Распределённая согласованность |
| Границы domain | Могут отсутствовать | Явные | Обязательны |
| Независимое масштабирование | Ограничено | Ограничено | Сильная сторона |
| Независимые команды | Ограниченно | Хорошо до определённого масштаба | Сильная сторона |
| Observability | Проще | Проще | Значительно сложнее |
| Network failures внутри domain | Нет | Нет | Да |
| Independent deployment | Нет | Нет | Да |
| Fault isolation | Ограничено | Частично | Лучше при правильном дизайне |
| Стоимость инфраструктуры | Ниже | Ниже | Выше |
| Требования к DevOps maturity | Ниже | Умеренные | Высокие |
| Возможность эволюции | Зависит от структуры | Очень хорошая | Хорошая при верных границах |
Из этой таблицы хорошо видно:
модульный монолит часто даёт значительную часть архитектурной дисциплины микросервисов без большей части распределённой стоимости.
Что мы бы выбрали для нового CRM
Если это новая CRM:
до нескольких тысяч пользователей;
одна команда;
один основной продукт;мы бы обычно начинали с:
Modular Monolith
+
PostgreSQL
+
Worker
+
Object Storage при необходимостиа не с десятка сервисов.
Что мы бы выбрали для нового SaaS
Аналогично:
Core Application
├── Identity
├── Organizations
├── Projects
├── Billing
├── Files
└── Notificationsс хорошими границами.
По мере роста можно отдельно вынести:
AI processing;media processing;billing;или другой subsystem, где появилась конкретная причина.
Что насчёт интернет-магазина
Можно начать с модулей:
Catalog
Orders
Inventory
Payments
Customersв одном приложении.
Если бизнес вырастет:
Catalogможет масштабироваться отдельно.
Inventory может интегрироваться с большим количеством складов.
Payments — стать самостоятельной финансовой подсистемой.
Но на первом этапе единая транзакционная модель может быть значительно проще и безопаснее.
А когда мы бы сразу серьёзно рассматривали микросервисы
Например если новый продукт фактически создаётся внутри уже крупной организации.
Есть:
несколько независимых команд;существующая платформа сервисов;централизованный monitoring;CI/CD;service discovery;message broker;platform engineering.И заранее известны устойчивые domain boundaries.
Тогда стоимость микросервисов уже частично оплачена существующей инфраструктурой.
Или если части системы имеют принципиально разные требования
Например продукт изначально состоит из:
Transactional APIи:
GPU inference cluster.Очевидно, запускать их как один process бессмысленно.
Здесь разделение существует не ради моды.
Оно следует из workload.
Ещё один хороший критерий — стоимость отказа
Если падение:
Recommendation Engineне должно останавливать:
Checkout,разделение может давать полезную fault isolation.
Но если два сервиса всё равно критически зависят друг от друга синхронно, физическое разделение не обязательно улучшит resilience.
Микросервисы не гарантируют отказоустойчивость автоматически
Можно создать:
10 сервисов,каждый из которых требует:
User Serviceдля любого запроса.
User Service упал.
Теперь:
все 10 сервисов
фактически недоступны.Мы создали распределённую архитектуру и один центральный bottleneck.
Resilience тоже нужно проектировать.
Иногда монолит имеет меньший blast radius, чем плохие микросервисы
Например одно приложение имеет локально доступные данные.
У него нет:
пяти сетевых hopsдля простого запроса.
Значит меньше зависимостей способно его разрушить.
Разделение само по себе не делает систему надёжнее.
Что следует измерить перед разделением
Вместо архитектурного спора полезно посмотреть на факты:
какой модуль создаёт нагрузку;какой модуль чаще меняется;какой модуль вызывает инциденты;где команды блокируют друг друга;какие deployment действительно должны быть независимыми.Если проблема измеряется — можно оценить, решит ли её сервис.
Хороший архитектурный вопрос
Не:
Получим ли мы пользу от микросервисов?
Она почти всегда найдётся.
А:
Будет ли эта польза больше стоимости распределённой системы?
Стоимость включает:
network;
retries;
idempotency;
observability;
deployment;
security;
data consistency;
testing;
local development;
operations.Вот это уже реальное сравнение.
Как понять, что монолит пока не мешает
Например:
deployment быстрый;tests укладываются в разумное время;команда понимает domain;модули имеют границы;масштабирование приложения работает;release одной функции не создаёт большой риск.Тогда сама по себе монолитность не является проблемой.
Не нужно создавать проблему, чтобы потом решить её микросервисами.
Как понять, что модуль пора выделять
Пример сильной комбинации:
Billing имеет чёткую domain boundary;его развивает отдельная команда;он масштабируется отдельно;у него собственный release cadence;прямые транзакции с core минимальны;есть mature monitoring/CI/CD.Теперь extraction выглядит экономически и технически оправданным.
А вот слабая комбинация
«Billing folder стал большим».И:
«Хотим современную архитектуру».Этого мало.
Самое неприятное последствие слишком ранних микросервисов
Они могут замедлить получение ответа на главный вопрос нового продукта:
Нужен ли он вообще пользователям?
Пока команда строит:
service discovery;
distributed tracing;
message broker;
API gateway;
CI/CD десяти сервисов,конкурент выпускает полезную функцию в обычном монолите.
На ранней стадии скорость изменения продукта часто важнее возможности независимо масштабировать компонент, который пока никто не использует.
Но и быстрый MVP не должен быть бесструктурным
Из идеи:
Сначала монолит.
не следует:
Можно писать как угодно.
Наоборот.
Если есть вероятность роста, именно модульная структура позволяет сохранить скорость сейчас и свободу позже.
Это компромисс, который мы чаще всего считаем наиболее практичным
Один deploy
+
Явные domain modules
+
Контролируемые зависимости
+
Ownership данных
+
Internal events
+
Тестируемые контрактыПока сеть между модулями не нужна.
Когда она действительно понадобится — архитектура уже подготовлена.
Можно сформулировать путь развития так
Неструктурированный монолитне является обязательной первой стадией.
Лучше:
Модульный монолит
↓
Выделенные workers
↓
Независимые инфраструктурные компоненты
↓
Отдельные сервисы там,
где появилась реальная причинаЭто эволюция.
Не революция.
Не нужно заранее знать финальную архитектуру
Это ещё один важный момент.
У продукта через три года могут появиться функции, которых сегодня нет даже в бизнес-плане.
Поэтому попытка уже сейчас нарисовать:
Service #1
...
Service #27создаёт иллюзию точности.
Гораздо полезнее спроектировать:
правильные текущие границы
+
возможность изменять их.Архитектура должна позволять ошибаться дёшево
Если границу модуля внутри монолита выбрали неправильно, её можно перенести относительно легко.
Если создали:
отдельный сервис;
отдельную БД;
внешний API;
message contracts;
deployment;
monitoring,перенос становится намного дороже.
На раннем этапе, когда неопределённость высока, обратимые решения особенно ценны.
Иногда лучшее архитектурное решение — пока ничего не разделять физически
Но подготовить:
границы;
контракты;
ownership;
events.Это не технический долг.
Это сознательное управление сложностью.
Что в итоге выбрать
Если новый проект — это:
CRM;
SaaS;
клиентский кабинет;
B2B-сервис;
интернет-магазин;
внутренняя система,и его делает одна небольшая команда, мы бы в большинстве случаев начинали с модульного монолита.
Это даёт:
простую разработку;
простые транзакции;
простой deployment;
простую диагностику;
низкую инфраструктурную стоимость.При этом правильные module boundaries оставляют возможность дальнейшего разделения.
Микросервисы становятся сильным решением позднее
Когда одновременно появляются:
доказанные domain boundaries;
независимые команды;
разный workload;
разный release cadence;
необходимость fault isolation;
реальная потребность в independent scaling.Тогда дополнительная distributed complexity начинает окупаться.
Вместо вывода
Монолит и микросервисы часто обсуждают так, будто это выбор между:
старой архитектуройи:
современной архитектурой.На практике всё намного интереснее.
Хорошо спроектированный модульный монолит может годами обслуживать серьёзный production-продукт.
А плохо спроектированные микросервисы могут превратить даже относительно простой SaaS в систему, где для открытия одной страницы требуется успешная работа шести процессов, трёх API и очереди сообщений.
Микросервисы дают реальные преимущества:
независимое масштабирование;
независимый deployment;
изоляцию отдельных нагрузок;
автономию больших команд;
разные технологические runtime.Но вместе с ними появляются:
сетевые сбои;
timeouts;
retries;
idempotency;
distributed consistency;
contract testing;
distributed tracing;
service-to-service security;
сложный deployment.Поэтому вопрос должен звучать не:
«Достаточно ли наш проект серьёзный для микросервисов?»
А:
«Какая конкретная проблема нашего продукта настолько серьёзна, что ради её решения мы готовы заплатить стоимость распределённой системы?»
Если такого ответа пока нет, это хороший сигнал не спешить.
Сделать приложение модульным.
Определить domain boundaries.
Ограничить зависимости.
Закрепить ownership данных.
Отделить тяжёлые background jobs.
Наблюдать за реальной нагрузкой и работой команды.
А затем разделять именно те части, которым это действительно нужно.
Так архитектура развивается вместе с продуктом.
Не раньше него.
И в долгосрочной перспективе именно это часто оказывается намного важнее количества прямоугольников со словом Service на архитектурной схеме.