Микросервисы: границы, данные и коммуникация
Микросервисы - это не "много маленьких папок" и не "монолит, разрезанный на десять процессов". Это архитектурный подход, где независимые сервисы имеют свои границы ответственности, данные, контракт и цикл релиза.
Главная сложность микросервисов не в том, чтобы запустить несколько HTTP-серверов. Главная сложность в распределенной системе: сеть может сломаться, данные живут в разных местах, один пользовательский сценарий проходит через несколько сервисов, а ошибка может быть не в вашем коде, а между сервисами.
Поэтому микросервисы нужно выбирать осознанно.
Когда микросервисы оправданы
Микросервисы могут быть полезны, когда:
- разные части продукта развиваются независимо;
- есть несколько команд;
- нагрузка на компоненты сильно отличается;
- нужны разные циклы релизов;
- часть системы требует отдельного масштабирования;
- монолит стал трудно менять из-за организационных границ.
Если проект маленький, хороший модульный монолит почти всегда проще. В нем меньше инфраструктуры, проще транзакции, легче дебажить и дешевле запускать.
Граница сервиса
Сервис должен отвечать за понятную бизнес-область:
auth-service- пользователи, пароли, токены, сессии;books-service- книги, отзывы, поиск;notification-service- email, push, Telegram-уведомления;billing-service- платежи, счета, подписки.
Плохие границы:
utils-service;database-service;common-service;validation-service.
Такие названия обычно означают, что граница проведена технически, а не по смыслу продукта.
Хороший сервис можно описать одним предложением: "этот сервис отвечает за ...". Если предложение распадается на пять несвязанных частей, граница слабая.
Database per service
Один из ключевых принципов: сервис владеет своими данными. Другие сервисы не читают его таблицы напрямую.
Плохо:
books-service -> auth.users table
Лучше:
books-service -> auth-service API
Почему это важно? Если books-service напрямую читает таблицу пользователей, auth-service уже не может свободно менять схему. База становится скрытым публичным API.
На практике иногда несколько сервисов физически живут в одной СУБД, но у каждого должна быть своя схема или набор таблиц, за которые он отвечает.
API Gateway
API Gateway принимает внешние запросы и направляет их во внутренние сервисы.
Он может делать:
- маршрутизацию;
- проверку access token;
- rate limiting;
- request id;
- базовое логирование;
- простую агрегацию ответов.
Пример:
frontend -> gateway -> auth-service -> books-service -> billing-service
Gateway не должен превращаться в огромный сервис с бизнес-логикой всего продукта. Если вся логика оказалась в gateway, микросервисы стали тонкими wrappers вокруг данных.
BFF
BFF означает Backend for Frontend. Это backend, заточенный под конкретный клиент: web, mobile, admin panel.
Например, web-странице нужен один формат данных, мобильному приложению - другой, админке - третий. Вместо того чтобы заставлять каждый клиент собирать данные из пяти сервисов, можно сделать BFF:
web app -> web-bff -> internal services mobile -> mobile-bff -> internal services
BFF полезен, когда клиенты реально отличаются. Если различий нет, отдельный BFF может быть лишней сложностью.
Синхронная коммуникация
HTTP или gRPC подходят, когда клиенту нужен ответ прямо сейчас:
frontend -> gateway -> books-service
Плюсы:
- просто понять;
- легко вызвать;
- ответ приходит сразу;
- удобно для query-запросов.
Минусы:
- зависимость от доступности другого сервиса;
- каскадные сбои;
- рост latency;
- необходимость таймаутов и retry.
Если один запрос проходит через пять сервисов, общая задержка складывается. Если один из них недоступен, весь сценарий может упасть.
Асинхронная коммуникация
Очереди и события подходят, когда операцию можно выполнить позже:
orders-service -> OrderCreated event -> notification-service
Плюсы:
- сервисы слабее связаны;
- нагрузку можно сглаживать;
- временная недоступность получателя не всегда ломает основной сценарий;
- можно добавить нового обработчика события без изменения отправителя.
Минусы:
- сложнее отлаживать;
- появляются повторные доставки;
- результат не мгновенный;
- нужна идемпотентность;
- нужен dead-letter или другой механизм для плохих сообщений.
События
Событие описывает факт, который уже произошел:
BookCreated OrderPaid UserRegistered
Плохо:
CreateBook SendEmail
Это уже команды, а не события.
Событие обычно содержит:
event_id;- тип события;
- время;
- id сущности;
- минимальные данные, нужные получателям;
- версию схемы.
Пример:
{ "event_id": "7f7c9c2e-9a2a-4a49-9b8a-bf6c2f1d0d9b", "type": "BookCreated", "created_at": "2026-05-23T10:00:00Z", "book_id": "book_123", "title": "Distributed Systems" }
Idempotency
Очереди часто доставляют сообщение больше одного раза. Обработчик должен выдерживать повторную доставку.
Идемпотентный обработчик не ломает состояние, если событие пришло дважды:
if event_id already processed: skip else: handle event and save event_id
Без этого повторное событие может отправить два email, дважды начислить бонус или создать дубликат записи.
Типичные ошибки
- Делить систему на микросервисы слишком рано.
- Делать сервисы вокруг технических слоев, а не бизнес-областей.
- Использовать общую базу как интеграционный слой.
- Не ставить timeout на вызовы других сервисов.
- Повторять неидемпотентные операции без ключа.
- Прятать всю бизнес-логику в gateway.
- Не иметь request id и distributed tracing.
Чеклист микросервиса
- У сервиса есть четкая область ответственности.
- У сервиса свои данные или своя схема данных.
- Внешний контракт описан.
- Есть healthcheck и readiness.
- Есть structured logs с request id.
- Есть таймауты на внешние вызовы.
- Retry ограничены и не ломают состояние.
- Повторные события обрабатываются безопасно.
- Общие библиотеки не тащат бизнес-логику между сервисами.
- Команда понимает, зачем этот сервис отделен от монолита.