В двух сервисах одна таблица изменяется напрямую. Какой архитектурный дефект это создаёт?
Это создаёт размытую ответственность за данные: у таблицы нет единственного владельца, который контролирует инварианты, схему и правила изменения. В результате сервисы начинают неявно зависеть от внутренней модели друг друга, а любое изменение может нарушить логику соседнего компонента.
В монолитах общий доступ к таблицам часто был приемлемым: один процесс контролировал транзакции, схему и бизнес-правила. При переходе к сервисной архитектуре компоненты стали изменяться и развёртываться независимо, поэтому потребовалось явно определить владельца каждого набора данных.
Принцип владения данными появился как средство уменьшить связанность между сервисами. Сервис-владелец отвечает за операции над своими данными и предоставляет другим компонентам доступ через контракт, а не через прямое изменение таблиц.
Если два сервиса напрямую изменяют одну таблицу, каждый из них может считать допустимыми собственные переходы состояния. Один компонент способен обойти проверки другого, изменить формат поля или удалить данные, от которых зависит чужая логика.
Такая схема также связывает релизы: изменение таблицы становится межсервисным изменением, даже если формально меняется только один сервис. Труднее определить, где исправлять ошибку, кто отвечает за миграцию и какой компонент должен обеспечивать целостность данных.
Нужно назначить единственный сервис-владелец таблицы или логически принадлежащего ему набора данных. Только он выполняет изменения и отвечает за инварианты, проверки, миграции и совместимость модели.
Другие сервисы обращаются к владельцу через публичный API либо отправляют ему команды через очередь или шину событий. Для чтения возможны отдельные проекции, кэш или реплика данных, но такая копия не должна становиться вторым источником истины.
Важно различать владение данными и физическое размещение. Даже если несколько сущностей находятся в одной базе, границы ответственности могут быть логическими. Однако прямой доступ к таблицам чужого домена всё равно создаёт связанность и должен рассматриваться как временное архитектурное исключение.
При выборе взаимодействия учитывают компромиссы. Синхронный API проще использовать и позволяет сразу получить результат, но увеличивает связанность и чувствительность к доступности владельца. Асинхронные команды и события снижают связанность и лучше подходят для независимого масштабирования, но добавляют задержки, повторную обработку и необходимость разрешать временную несогласованность.
Если двум операциям действительно нужна атомарность над одними данными, их обычно следует оставить в одном владеющем компоненте. Распределённая транзакция между сервисами не должна быть способом компенсировать неправильно выбранную границу; чаще применяют локальную транзакцию владельца и надёжную публикацию события после изменения.
В системе заказов сервис заказов и сервис доставки напрямую обновляли поля одной таблицы. Доставка меняла статус заказа после получения посылки, а сервис заказов параллельно разрешал отмену. Иногда отменённый заказ переходил в статус доставки, потому что оба компонента проверяли допустимость перехода по-разному.
Рассматривались три варианта. Общая блокировка таблицы могла быстро остановить конфликт, но сохранила бы связанность и не решила проблему разных бизнес-правил. Передача всей таблицы под управление сервиса заказов упростила бы контроль, но потребовала бы изменить интеграцию доставки. Разделение данных с обменом событиями улучшило бы границы, однако добавило бы задержку и обработку повторов.
Выбрали сервис заказов владельцем жизненного цикла заказа. Сервис доставки отправляет команды и получает разрешённые статусы через контракт, а изменения заказа публикуются событиями. Это устранило конкурирующие правила переходов; взамен система явно обрабатывает задержку доставки события и повторную обработку сообщений.
Нет, запрет записи решает только наиболее опасную часть проблемы. Прямое чтение также связывает сервис с внутренней схемой владельца: переименование поля, нормализация данных или изменение смысла значения может сломать потребителя.
Для независимости сервис должен читать стабильный контракт владельца или специально подготовленную проекцию. Допустим временный прямой доступ при миграции, но его следует считать техническим долгом с владельцем и сроком устранения.
Не обязательно. Копия допустима, если она является производной проекцией, а не альтернативным источником истины. Второй сервис может хранить нужные ему поля для быстрого чтения, но изменения исходных данных должны проходить через владельца.
Цена такого решения — временная несогласованность, задержка доставки событий, необходимость идемпотентной обработки и восстановления проекции. Это осознанный компромисс между автономностью чтения и мгновенной согласованностью.
Её нужно представить как последовательность локальных операций с явными состояниями и компенсацией, а не автоматически пытаться выполнить одну распределённую транзакцию. Например, первый владелец фиксирует заказ, затем второй резервирует ресурс, после чего система подтверждает результат или выполняет компенсирующее действие.
Такой подход требует идентификатора операции, повторяемости команд, обработки тайм-аутов и наблюдаемости промежуточных состояний. Если же операция постоянно требует атомарного изменения обоих наборов данных, это сильный сигнал, что граница между владельцами выбрана неудачно и домены стоит пересмотреть.