Каждый раз, когда команда начинает новый проект, рано или поздно всплывает этот вопрос. Кто-то насмотрелся докладов про Netflix и Amazon и хочет сразу микросервисы. Кто-то наоборот — обжёгся на распределённом хаосе и говорит «только монолит». Правда, как обычно, посередине, и зависит от конкретной ситуации.

Что такое монолит на самом деле

Монолит — это не ругательство. Это когда всё приложение живёт в одном процессе: авторизация, каталог товаров, платежи, уведомления — всё вместе, деплоится одной командой.

Плюсы очевидны:

  • Просто запустить локально. npm start или python manage.py runserver — и ты в деле.
  • Нет сетевых вызовов между компонентами. Функция вызывает функцию напрямую, без HTTP и таймаутов.
  • Легко дебажить. Один стектрейс, один лог, одна точка отказа.
  • Транзакции работают нормально. Не нужно городить distributed transactions.

И минусы тоже понятны: со временем код превращается в «большой комок грязи» (big ball of mud), деплой тормозит, команды мешают друг другу в одной кодовой базе.

Но вот вопрос: когда именно это становится проблемой?

Микросервисы — это не архитектура, это операционная сложность

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

Когда это даёт преимущества:

  • Разные команды могут деплоить независимо.
  • Можно масштабировать только нагруженные части. Каталог товаров грузят больше, чем личный кабинет — масштабируй только его.
  • Технологический зоопарк становится управляемым. Один сервис на Go, другой на Python — и это нормально.
  • Отказ одного сервиса не кладёт всё приложение (при правильной архитектуре).

Но за это платишь дорого:

  • Нужен оркестратор: Kubernetes или хотя бы Docker Compose с нормальным мониторингом.
  • Каждый сервис — это свой CI/CD, свои логи, своя база данных (идеально).
  • Распределённые транзакции — это боль. Паттерны Saga, двухфазный коммит, eventual consistency — звучит умно, реализуется с матами.
  • Сетевые вызовы добавляют латентность и точки отказа. То, что раньше работало за 1 мс как вызов функции, теперь занимает 10-50 мс через HTTP.
  • Дебаггинг распределённых систем требует distributed tracing (Jaeger, Zipkin), иначе найти причину бага — квест.

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

Когда монолит — правильный выбор

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

Из практики: команды, которые начинают с микросервисов на старте, в среднем тратят 30-40% времени не на бизнес-логику, а на инфраструктуру. Это деньги и время, выброшенные на этапе, когда продукт ещё не нашёл своего рынка.

Маленькая команда (до 5-7 разработчиков). Смысл микросервисов — разные команды работают независимо. Если команда маленькая, проблемы координации нет. Все друг друга знают, коммуникация прямая, конфликты в коде решаются разговором, а не версионированием API.

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

Нет инфраструктурной команды. Kubernetes, service mesh (Istio или Linkerd), централизованное логирование (ELK-стек), distributed tracing — всем этим нужно кто-то управлять. Это не настраивается один раз и забывается. Если нет DevOps-инженера или платформенной команды — поддержка этого зоопарка съест всё время.

Когда пора резать

Ответить на этот вопрос помогают несколько конкретных сигналов.

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

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

Разные части системы требуют разного стека. ML-модель хочет Python, высоконагруженный API — Go, фронтенд-логика — Node.js. В монолите это сложно сочетать. В сервисах — естественно.

Цикл деплоя стал долгим. Тесты прогоняются 40 минут, деплой — ещё 20. При этом каждое изменение, даже маленькое, требует прогнать всё это. Когда продуктивность падает из-за долгого pipeline — это сигнал к разделению.

Части системы имеют разные требования к надёжности. Платёжный модуль должен работать при 99.99% uptime. Модуль рекомендаций можно деградировать без критических последствий. Разделение позволяет применять разные стратегии отказоустойчивости к разным частям.

Как правильно нарезать, если решил

Самая частая ошибка — нарезать по техническому слою. «Вот сервис для работы с базой», «вот сервис для бизнес-логики», «вот API-шлюз». Это антипаттерн. Такие сервисы не могут жить независимо — изменение одной фичи затрагивает все три.

Правильный принцип — нарезать по бизнес-доменам (Domain-Driven Design, bounded contexts). Один сервис = одна область ответственности с точки зрения бизнеса.

Пример для интернет-магазина:

  • Каталог — товары, категории, поиск, фильтры.
  • Корзина и заказы — добавление в корзину, оформление заказа, история.
  • Платежи — всё что связано с деньгами, изолировано максимально.
  • Уведомления — email, SMS, push. Не знает про бизнес-логику, просто отправляет.
  • Пользователи — регистрация, авторизация, профили.

Каждый из этих доменов может деплоиться независимо, иметь свою базу данных и масштабироваться отдельно.

Промежуточный путь — модульный монолит

Есть вариант, о котором редко говорят в хайповых докладах, но который часто оказывается оптимальным — модульный монолит (modular monolith).

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

Что это даёт:

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

Шаблон «сначала модульный монолит, потом выделяй сервисы по мере роста» — это то, что реально работает в большинстве компаний среднего размера. Netflix и Amazon пришли к микросервисам после нескольких лет работы в монолите — когда масштаб и команды реально потребовали этого.

Практические цифры

Несколько ориентиров, которые помогают принять решение:

  • Меньше 5 разработчиков — почти всегда монолит или модульный монолит.
  • 5-15 разработчиков, 2-3 команды — модульный монолит или начало выделения самых нагруженных/специфичных сервисов.
  • Больше 15 разработчиков, независимые команды с разными roadmap — микросервисы оправданы.
  • Трафик меньше 1000 RPS в пиковой нагрузке — горизонтальное масштабирование монолита справится.
  • Суточная аудитория меньше 100 000 пользователей — скорее всего, монолит масштабируется нормально.

Эти цифры — не догма, но хороший способ приземлить разговор об архитектуре.

Что часто упускают при переходе

Data management. В монолите одна база, в микросервисах каждый сервис должен иметь свою (Database per Service). Иначе сервисы снова связаны через схему базы данных. Переход к раздельным базам — это миграция данных, синхронизация, eventual consistency. Это работа на месяцы.

API versioning. Когда сервисы общаются между собой — нужно версионировать API. Поменял контракт — все консьюмеры должны обновиться. Это организационная нагрузка, которой не было в монолите.

Тестирование. В монолите интеграционный тест — это один процесс. В микросервисах нужны contract tests (Pact, Spring Cloud Contract), нужна тестовая среда с поднятыми сервисами, нужна стратегия для тестирования взаимодействий. Всё это требует времени на настройку и поддержку.

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

Как это выглядит в реальном проекте

Предположим, к нам в REEXY приходит клиент с запросом на интернет-магазин. Стартовая цена от 10 000 ₽ — это всегда монолит с нормальной модульной структурой. Не потому что дёшево, а потому что это правильно для старта: MVP нужно запустить быстро, протестировать на пользователях, понять что работает.

Если через год магазин вырос, появилась отдельная команда контента, отдельная команда разработки, нагрузка выросла — тогда обсуждаем, что и зачем выделять. Платёжный модуль — почти всегда первый кандидат на изоляцию, из соображений безопасности и соответствия требованиям (PCI DSS). Поиск — если нагружен и нужен Elasticsearch — второй.

Выделение сервисов — это инвестиция. Как и любая инвестиция, она должна окупаться. «Потому что Netflix так делает» — не окупаемость.

Простое правило для принятия решения

Если не знаешь с чего начать — вот простая логика:

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

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