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

Сервис предоставляет другим сервисам CRUD доступ к своим внутренним сущностям. Какой главный риск это созда...

Сервис предоставляет другим сервисам CRUD-доступ к своим внутренним сущностям. Какой главный риск это создаёт для границы сервиса?

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

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

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

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

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

В монолитах общий доступ к объектам и базе данных часто был удобен: разные части приложения могли напрямую читать и изменять одну модель. При переходе к распределённой архитектуре такое удобство превращается в связанность через сеть и независимые хранилища.

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

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

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

Это создаёт несколько последствий:

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

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

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

Граница сервиса должна строиться вокруг ответственности и инвариантов, а не вокруг отдельных таблиц. Внешний контракт описывает намерение или устойчивый результат бизнес-операции: например, «отменить заказ» вместо «установить полю status значение CANCELLED».

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

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

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

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

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

Сервис заказов предоставил сервису доставки CRUD-доступ к заказу. Доставка начала читать поле status, менять его на READY после формирования маршрута и сохранять собственные признаки прямо в той же модели. Позже сервис заказов добавил промежуточные статусы для оплаты, а значение READY стало означать только готовность заказа к сборке.

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

Выбрали контракт «заказ готов к передаче в доставку» со стороны сервиса заказов и отдельную команду доставки «создать доставку». Сервис заказов остался владельцем состояния заказа, сервис доставки — владельцем состояния доставки. В результате внутренние статусы можно было менять независимо, а связь между сервисами выражалась через бизнес-смысл, а не через общую запись.

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

1. Допустим ли API, возвращающий те же поля, что и внутренняя таблица, если потребители имеют только права на чтение?

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

2. Чем бизнес-команда отличается от обычного обновления ресурса?

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

3. Не приводит ли отказ от общего CRUD к чрезмерному количеству API-методов?

Такой риск существует: слишком детализированный контракт может превратиться в набор методов для каждого внутреннего шага. Баланс достигается выделением устойчивых бизнес-возможностей, а не публикацией каждого изменения поля. Контракт должен быть достаточно выразительным для потребителя, но не обязан раскрывать все внутренние операции сервиса.