АрхитектураМикросервисы и интеграцииАрхитектор микросервисных систем

Как бизнес инварианты помогают провести границу микросервиса?

Как бизнес-инварианты помогают провести границу микросервиса?

Проходите собеседования с ИИ помощником Hintsage

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

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

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

Исторический контекст

Идея опираться на бизнес-смысл, а не на технические слои или отдельные таблицы, связана с развитием предметно-ориентированного проектирования и концепцией ограниченного контекста. Она возникла как ответ на системы, где общая модель данных незаметно связывала разные части бизнеса и делала изменения рискованными.

В микросервисной архитектуре эта проблема усиливается: отдельный сервис должен иметь самостоятельную ответственность и возможность развиваться независимо. Поэтому границы выбирают вокруг согласованных правил предметной области, а не просто вокруг набора CRUD-операций.

Постановка проблемы

Предположим, остаток товара, резервирование и списание находятся в разных сервисах, хотя правило «доступный остаток не может стать отрицательным» должно соблюдаться атомарно. При параллельных запросах сервисы могут принять решения на устаревших данных и временно или постоянно нарушить этот инвариант.

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

Неверно выбранная граница приводит к распределённым транзакциям там, где их можно было избежать, частым синхронным вызовам, дублированию правил и неясному владению данными.

Подробное решение

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

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

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

Признаки хорошей границы:

  • сервис имеет одного понятного владельца бизнес-правил;
  • его данные не требуется изменять напрямую из других сервисов;
  • большинство операций внутри границы можно завершить локально;
  • внешние интеграции выражают намерения или факты, а не раскрывают внутреннюю структуру хранения;
  • изменения в одной области редко требуют синхронной правки другой.

Граница не обязана совпадать с одной сущностью или таблицей. Она может включать несколько сущностей, если они совместно поддерживают один инвариант. И наоборот, одна крупная бизнес-сущность может быть представлена в разных контекстах разными моделями и не должна автоматически становиться общей сущностью всех сервисов.

Есть компромисс между автономностью и дублированием. Иногда потребителю нужна локальная копия данных для быстрых чтений; тогда допускают денормализацию и eventual consistency, но источник истины и правила изменения должны оставаться однозначными. Если же требование к немедленной согласованности действительно сквозное, следует либо пересмотреть границы, либо явно принять координацию и её стоимость.

Ситуация из практики

В интернет-магазине команда сначала разделила систему по таблицам: отдельные сервисы отвечали за заказ, позиции заказа и скидки. Расчёт итоговой цены требовал последовательных вызовов между сервисами, а изменение правила скидки часто приводило к рассинхронизации суммы заказа и отображаемой цены.

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

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

Что кандидаты часто упускают

1. Должен ли каждый инвариант находиться в одном сервисе?

Нет, это не абсолютное правило для любого бизнес-требования. Важно различать инварианты, которые должны соблюдаться атомарно в рамках одной операции, и правила, допускающие промежуточное состояние или асинхронное согласование.

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

2. Как понять, что сервис стал слишком большим?

Размер кода или число таблиц сами по себе не являются достаточными признаками. Более полезны сигналы: несколько независимых владельцев правил, несвязанные темпы изменений, постоянные конфликты при разработке и необходимость выпускать весь сервис ради изменений только одной области.

Такой сервис может содержать несколько будущих ограниченных контекстов. Однако разделять его стоит не по каждой сущности, а только когда появится самостоятельная бизнес-ответственность и понятный контракт между частями; чрезмерное дробление создаёт сетевые вызовы и распределённую согласованность без реальной пользы.

3. Можно ли передавать другому сервису данные для проверки инварианта?

Передавать данные можно, но это не передаёт владение правилом. Если сервис-потребитель самостоятельно интерпретирует чужое состояние и принимает решение, правило начинает дублироваться, а устаревшие данные могут привести к неверному результату.

Надёжнее передавать владельцу намерение, например запрос на резервирование, и получать результат операции. Допустимо публиковать события или проекции для чтения, но окончательная проверка и изменение состояния, от которых зависит инвариант, должны выполняться сервисом-владельцем.