> Какие доводы и обстоятельства влияют на решение о делении или объединении сервисов (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Employcity

Стек: Go

> Пример ответа

Короткий ответ

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

Подробное объяснение

Основные доводы за деление:

  • Независимость масштабирования - разные профили нагрузки (например, чтение vs запись, CPU-bound vs IO-bound).
  • Независимый деплой - возможность выпускать изменения в одном сервисе без каскадного релиза остальных.
  • Изоляция сбоев - падение одного сервиса не роняет всю систему (blast radius).
  • Командная автономия - каждая команда владеет своим сервисом end-to-end, включая базу данных.
  • Разные SLA - критичный сервис может иметь более высокие требования к доступности, чем остальные.
  • Технологическая свобода - разные сервисы могут использовать разные языки, хранилища, очереди.

Доводы за объединение:

  • Транзакционная целостность - распределённые транзакции (Saga, outbox) сложны и хрупки; если данные всегда меняются атомарно - это один сервис.
  • Сетевые задержки - каждый вызов через сеть добавляет latency; если сервисы обмениваются большими объёмами данных - объединение выгоднее.
  • Операционная нагрузка - каждый сервис требует CI/CD, мониторинг, алерты, логирование, управление секретами, on-call.
  • Когнитивная сложность - слишком много сервисов делает систему нечитаемой; разработчики не могут удержать в голове все взаимодействия.
  • Дублирование данных - если два сервиса постоянно синхронизируют одни и те же данные, это признак того, что граница проведена неправильно.
  • Отсутствие независимости - если изменение в одном сервисе всегда требует изменения в другом (например, из-за контракта API), деление не даёт выгоды.

Ключевые обстоятельства, которые влияют на решение:

  • Размер команды - для 3-5 разработчиков один монолит эффективнее, чем 10 микросервисов.
  • Стадия продукта - на ранней стадии неизвестны границы домена, преждевременное деление создаёт ложные абстракции.
  • Зрелость инфраструктуры - нужны service mesh, observability, feature flags, иначе деление принесёт больше проблем.
  • Частота изменений - если два модуля меняются вместе в 90% коммитов, их разделение искусственно.
  • Требования к compliance - разные уровни безопасности или регуляторные требования могут вынудить разделение.

На практике

Практический подход - начинать с монолита с чёткими модульными границами (modular monolith). Выделять сервис только тогда, когда появляется конкретный драйвер: команда упирается в деплой, нагрузка требует отдельного масштабирования, или нужна изоляция сбоев.

При делении использовать Domain-Driven Design: bounded context - естественная граница сервиса. Проверять границу через тест на независимость: можно ли изменить один сервис и задеплоить его без изменения другого? Если нет - граница неверна.

При объединении - смотреть на частоту совместных изменений (coupling) и на то, сколько трафика проходит между сервисами. Если 80% запросов к сервису A заканчиваются вызовом сервиса B - это один сервис.

На практике часто применяют правило: сервис должен быть размером с команду - если команда не может поддерживать сервис, он слишком большой; если сервис не даёт команде автономии - он слишком маленький.

Пример кода

Не применимо - это архитектурное решение, а не код. Но можно привести пример проверки границы через интерфейс:

GO
// Плохая граница: сервис Order всегда вызывает сервис Inventory
type OrderService struct {
inventory *InventoryClient
}
func (o *OrderService) CreateOrder(ctx context.Context, items []Item) error {
// каждый вызов - сетевой round-trip
for _, item := range items {
if err := o.inventory.Reserve(ctx, item); err != nil {
return err
}
}
// если Reserve и CreateOrder должны быть атомарны - это один сервис
return o.saveOrder(ctx, items)
}

Как отвечать на собеседовании

Начни с главного принципа: граница сервиса - это граница бизнес-способности, а не технического слоя. Затем перечисли ключевые trade-off: независимость vs транзакционность, масштабирование vs операционная нагрузка, автономия команд vs когнитивная сложность.

Приведи конкретный пример из практики: как ты решал, делить или объединять. Опиши обстоятельства - размер команды, частота изменений, требования к latency. Покажи, что решение принималось на основе данных, а не догмы.

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

Что проверяет интервьюер

Интервьюер оценивает:

  • Понимание trade-off, а не заученных списков - можешь ли ты объяснить, почему в конкретной ситуации деление вредно.
  • Умение связывать архитектуру с бизнес-метриками: скорость поставки, стоимость владения, time-to-market.
  • Практический опыт - приводишь ли ты реальные кейсы или только теорию.
  • Способность мыслить системно - учитываешь ли команду, инфраструктуру, стадию продукта, а не только технические параметры.
  • Осознание анти-паттернов: distributed monolith, синхронные цепочки вызовов, разделение по слоям.

Типичные ошибки

  • Деление по техническим слоям - отдельный сервис для БД, отдельный для бизнес-логики, отдельный для API. Это всегда анти-паттерн.
  • Игнорирование транзакций - утверждение, что "можно всё решить через Saga", без оценки сложности компенсаций и идемпотентности.
  • Деление ради деления - "у нас микросервисы, потому что так модно" или "чтобы в резюме было".
  • Неучёт команды - проектирование архитектуры для 50 разработчиков, когда в команде 5 человек.
  • Забывание про сеть - не учитывается, что каждый вызов - это latency, возможный timeout, retry, circuit breaker.
  • Объединение из-за лени - "проще добавить поле в существующий сервис", чем провести нормальный анализ границ домена.
  • Отсутствие критериев - нет чёткого порога, когда деление оправдано; решение принимается на эмоциях.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью