Микросервисы решают организационную задачу
Первое, что стоит сказать на собеседовании: микросервисы — это не про производительность и не про «современно». Технически распределённая система всегда медленнее и ненадёжнее монолита той же функциональности: вызов внутри процесса стоит наносекунды и не падает, сетевой вызов стоит миллисекунды и падает регулярно.
Что микросервисы действительно покупают: независимый деплой (команда выкатывает свой кусок, не согласуя релиз со всеми), независимое масштабирование (сервису отчётов нужно много CPU, сервису аутентификации — нет), изоляцию отказов (при грамотных границах падение отчётов не роняет оплату), автономию команд и свободу выбора технологий.
Что они за это берут: сеть между вашими модулями, распределённые транзакции, которых больше нет (см. сагу), необходимость наблюдаемости и трассировки, версионирование контрактов, сложное тестирование, отдельный контур эксплуатации и дежурств. Это постоянный налог, а не разовая инвестиция.
Отсюда практический критерий: микросервисы оправданы, когда организация упирается в согласование релизов и владения, а не когда код стал большим. Закон Конвея работает в обе стороны — архитектура повторяет структуру коммуникации команд, поэтому резать систему без изменения границ ответственности бессмысленно.
Модульный монолит как разумная точка старта
Совет, за который на интервью ставят плюс: начинать с монолита, но модульного. Границы модулей внутри одного процесса обходятся дёшево — их можно переносить рефакторингом, пока вы ещё не поняли предметную область. Границы сервисов стоят дорого: неверно проведённая граница закрепляется сетевым контрактом, и переносить её приходится через миграцию данных и согласованный релиз.
Практика модульного монолита: пакет на модуль, публичный API модуля явно выделен, доступ во внутренности запрещён (в Java это можно проверять ArchUnit или модулями JPMS), у каждого модуля свой набор таблиц и никаких JOIN через границу. Такой монолит режется на сервисы, когда действительно понадобится, — по уже существующим швам.
Обратный сценарий тоже встречается и его полезно уметь назвать: если микросервисы уже нарезаны неудачно, консолидация нескольких сервисов обратно в один — нормальное инженерное решение, а не признание поражения.
Отдельно стоит отличать «монолит» от «легаси»: большой хорошо структурированный монолит с чёткими модулями лучше десятка сервисов, связанных синхронными вызовами и общей базой.
Границы: bounded context вместо сущностей
Самая частая ошибка нарезки — резать по сущностям или по техническим слоям: сервис пользователей, сервис заказов, сервис уведомлений, а рядом «сервис базы данных» и «сервис бизнес-логики». Слои дают цепочки синхронных вызовов на каждый сценарий, а сущности — ложную общность.
DDD предлагает резать по bounded context — области, внутри которой термины предметной языка означают одно и то же. Ключевое наблюдение: одно и то же слово в разных контекстах — это разные модели. «Заказ» для оформления — корзина с ценами и промокодами; «заказ» для склада — набор позиций с весом и адресом; «заказ» для бухгалтерии — документ с НДС. Попытка сделать один общий класс Order, устраивающий всех, порождает объект с сорока полями, который меняется по любой причине.
Внутри контекста границей транзакции служит агрегат — группа объектов, у которой есть корень и инвариант, который надо соблюдать атомарно. Правило большого пальца: одна транзакция изменяет один агрегат; между агрегатами и тем более между контекстами — согласованность в конечном счёте.
Практический критерий хорошей границы: типичный бизнес-сценарий выполняется внутри одного сервиса, а не собирается из пяти синхронных вызовов. Если для показа одной страницы нужно обойти четыре сервиса — граница проведена не там.
Распределённый монолит
Худший из миров: цена распределённой системы уплачена, а её преимущества не получены. Это самый частый реальный исход неудачного перехода, и уметь описать его признаки полезнее, чем знать определение микросервисов.
Признаки. Сервисы нельзя выкатить по отдельности — релиз собирается «поездом», потому что контракты меняются согласованно. Один пользовательский сценарий проходит длинную цепочку синхронных HTTP-вызовов, и время ответа складывается, а доступность перемножается. Несколько сервисов ходят в одну и ту же базу и знают чужие таблицы. Изменение одного поля требует правок в четырёх репозиториях. Общая библиотека с доменными классами, обновление которой обязательно для всех.
Про перемножение доступности стоит держать цифру в голове: цепочка из пяти сервисов с доступностью 99,9% каждый даёт примерно 99,5% — то есть в пять раз больше простоя, чем у одного. Синхронные цепочки — главный источник каскадных отказов, и лечатся они не только таймаутами и circuit breaker, но в первую очередь пересмотром границ и переходом на асинхронное взаимодействие там, где ответ не нужен немедленно.
Отдельная ловушка — общая база данных. Она выглядит удобной (JOIN работает, транзакция одна), но убивает главное преимущество: схему нельзя менять независимо, и любой сервис может незаметно сломать чужие инварианты. Правило «база на сервис» существует именно поэтому, а не ради красоты.
Данные и связь между сервисами
Если у каждого сервиса своя база, немедленно возникают два вопроса: как получить данные, лежащие в чужом сервисе, и как обеспечить согласованность.
Для чтения есть три подхода. Синхронный запрос к владельцу — просто, но добавляет связность и задержку и делает вас зависимым от его доступности. Локальная реплика нужных данных, наполняемая событиями (event-carried state transfer) — быстро и автономно, но данные слегка устаревшие и их надо поддерживать. Отдельная модель чтения, собранная из событий нескольких сервисов, — это уже CQRS, ему посвящён отдельный урок.
Для записи распределённых транзакций не существует: 2PC в микросервисах не применяют из-за блокирующего координатора и деградации доступности. Вместо этого — сага из локальных транзакций с компенсациями, а для надёжной публикации событий — transactional outbox. Механика подробно разобрана в уроке про сагу и CDC.
Общее правило проектирования, которое стоит произнести: явно решите, где у вас строгая согласованность (внутри агрегата и его транзакции), а где согласованность в конечном счёте (между сервисами), и отразите это в API и в UI. Промежуточные состояния вроде payment_pending — это часть контракта, а не временное неудобство.
Что придётся построить вокруг
Переход к микросервисам оплачивается инфраструктурой, и на lead-интервью проверяют, помните ли вы про эту часть счёта.
Обязательный минимум: автоматизированный деплой на каждый сервис отдельно; централизованные логи с correlation id; метрики и распределённая трассировка, иначе отладка сценария через пять сервисов невозможна; service discovery и конфигурация; шлюз на входе с аутентификацией и лимитами; политика ретраев, таймаутов и circuit breaker по умолчанию.
Тестирование меняется качественно: end-to-end тесты через все сервисы становятся медленными и хрупкими, поэтому опорой становятся контрактные тесты (consumer-driven contracts) — потребитель декларирует свои ожидания, а сборка поставщика проверяет их. Это единственный способ менять контракты без релизного поезда.
И организационная часть: у каждого сервиса должен быть явный владелец — команда, которая его пишет, выкатывает и дежурит по нему. Сервис без владельца быстро превращается в общую свалку и главный источник инцидентов.
Как отвечать на собеседовании
Начинайте с честного trade-off, а не с плюсов: микросервисы покупают независимость деплоя, масштабирования и владения, а платят сетью, распределёнными данными и эксплуатацией. Это сразу задаёт уровень разговора.
На вопрос «как бы вы разрезали систему» отвечайте через bounded context и бизнес-возможности, а не через сущности и слои. Приведите пример с разным смыслом слова «заказ» в оформлении, на складе и в бухгалтерии — он убедительнее любого определения.
Обязательно упомяните распределённый монолит и его признаки: общая база, синхронные цепочки, релизный поезд, общая доменная библиотека. Это показывает, что вы видели последствия, а не только слайды.
На вопрос «монолит или микросервисы для нового продукта» уверенно отвечайте: модульный монолит с явными границами, а сервисы выделять по мере того, как границы устоятся и появится организационная потребность. И добавьте, что консолидация обратно — тоже нормальное решение.