К содержимому
14 авг. 2026 г.·7 мин чтения

Монолит или микросервисы с точки зрения базы данных

Выбор между монолитом и микросервисами решает владение данными: нанесите транзакции на карту, найдите общие таблицы и отделяйте реальные границы.

Монолит или микросервисы с точки зрения базы данных

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

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

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

Карта транзакций полезнее графа зависимостей

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

Начинайте с операций, а не таблиц. Для каждой команды, которая меняет состояние, запишите инициатора, прочитанные и записанные таблицы, блокировки, используемые ограничения и последствия частичного выполнения. Учтите задания, триггеры, хранимые процедуры, импорт файлов и операторские скрипты. Именно там часто исчезает предполагаемая граница.

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

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

Отслеживайте производственные запросы вместе с исходным кодом. Динамический SQL, команды ORM, ночные задания и хранимые процедуры могут ускользнуть от статического анализа. Таблица без видимых ссылок из приложения иногда питает закрытие месяца. Если удаление модуля через три дня делает бухгалтерский отчет неверным, модуль не изолирован.

Общие таблицы откладывают цену координации

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

Цена связана не с технической возможностью двух процессов читать таблицу, а с неясными полномочиями. Какой сервис вправе добавить ограничение? Кто решает, означает ли status = 4 упаковку или отправку? Какое развертывание отвечает за backfill? Кто восстанавливает таблицу, если одному сервису нужен возврат к прошлому моменту, а другой уже записал новое состояние? Общее хранилище превращает обычную работу со схемой в согласование календарей команд.

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

Этот запрос дает первичную картину владения в PostgreSQL:

SELECT table_schema, table_name,
       array_agg(DISTINCT application_name ORDER BY application_name) AS writers
FROM audit_statement_usage
WHERE command IN ('INSERT', 'UPDATE', 'DELETE')
GROUP BY table_schema, table_name
HAVING count(DISTINCT application_name) > 1
ORDER BY table_schema, table_name;

В PostgreSQL нет встроенной таблицы audit_statement_usage. Создайте ее из журналов аудита базы или телеметрии запросов, включив хотя бы application_name, команду, схему и таблицу. Важна форма результата: каждая строка с несколькими писателями требует решения о владении, а не нового эндпоинта. Не определяйте владельца только по ролям базы, если приложения используют общие учетные данные. Иначе доказательство ничего не стоит.

В здоровой целевой системе у каждой таблицы один полномочный писатель. Другие компоненты могут получать копии под свои запросы. У копии есть договоренность о свежести, ее можно перестроить. От формы общей таблицы молча зависят несколько сторон, и такой контракт увидеть гораздо труднее.

Требование к согласованности выбирает границу

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

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

Популярный совет поставить брокер между всеми компонентами неверен, если звучит до решения о согласованности. Брокер надежно переносит сообщения при заданных условиях. Он не решает, насколько резерв может отстать от заказа, что делать с повторным событием и кто чинит отсутствующую проекцию. Это семантика приложения. Если спрятать ее за publish(), у команды останется меньше доказательств.

Для каждой предполагаемой границы ответьте на четыре вопроса:

  1. Какое состояние пользователи или задания увидят между двумя коммитами?
  2. Как долго оно может оставаться несогласованным?
  3. Какая сторона повторяет запрос и как получатель распознает дубль?
  4. Какой процесс находит и исправляет сообщение, не создавшее нужное состояние?

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

Одна база не означает одного владельца

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

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

REVOKE ALL ON SCHEMA billing FROM fulfillment_app;
GRANT USAGE ON SCHEMA billing TO fulfillment_app;
GRANT SELECT ON billing.invoice_summary TO fulfillment_app;
REVOKE INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA billing FROM fulfillment_app;

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

Подход database-per-service тоже часто понимают неверно. Он означает, что сервис владеет контрактом хранения и другие сервисы не могут обойти его. Отдельный кластер для каждого малого процесса не требуется. Логические базы помогают с восстановлением и правами, а схемы в одном экземпляре PostgreSQL могут подойти на время выделения. Выбирайте изоляцию, которая закрепляет владение, но не умножает эксплуатацию до проверки самой границы.

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

Замена зависит от запроса. Экрану с текущим состоянием счета подойдет прямой запрос с таймаутом и заданным запасным поведением. Поиску, который фильтрует заказы по свойствам клиента, обычно нужна локальная проекция. Офлайн-отчету место в аналитическом хранилище. Копирование выбранных фактов дает намеренное дублирование; цепочка живых вызовов, случайно образовавшая распределенный join, скрывает связанность.

Пагинация выявляет частую ошибку. Допустим, клиент просит первые 50 заказов по риску клиента, но у заказов и риска разные владельцы. Нельзя прочитать 50 заказов, затем запросить риск и получить правильные первые 50. Чтение большего числа превращается в догадку с нестабильной производительностью. Перенесите ранжирование в собственную модель чтения или измените продуктовый контракт. Сетевой fan-out не сохраняет семантику базы благодаря оптимизму.

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

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

Событиям нужен атомарный источник истины

Найдите каждого скрытого писателя
Платформа одновременно читает все языки проекта, включая смешанный код больше миллиона строк.

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

Outbox помещает бизнес-запись и событие в один локальный коммит:

BEGIN;
UPDATE orders
SET status = 'confirmed', version = version + 1
WHERE id = :order_id AND status = 'pending';

INSERT INTO outbox_event (event_id, aggregate_id, event_type, payload, created_at)
VALUES (:event_id, :order_id, 'order.confirmed', :payload, CURRENT_TIMESTAMP);
COMMIT;

Relay публикует неотправленные строки и отмечает прогресс. При падении после отправки, но до отметки он может отправить событие дважды, поэтому потребителям нужна идемпотентность. Храните стабильный ID события и меняйте состояние потребителя, только если этот ID еще не обработан. Обещание exactly-once часто сводится к доставке at-least-once и устранению повторных эффектов. Называйте то, что действительно обеспечено.

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

Change data capture может питать relay, но захват сырых изменений таблиц не заменяет доменные события. Обновление строки сообщает об изменении хранилища. Оно не объясняет, подтвердили заказ, исправили, импортировали или отремонтировали. Потребители, которые выводят намерение из столбцов, привязываются к схеме, от которой вы хотели их освободить.

Отчеты раскрывают владение, скрытое командами

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

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

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

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

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

Безопасное выделение переносит владение раньше трафика

Уберите монолит с общими таблицами
CodeHero переписывает старые системы на Go и Postgres без копирования прежней структуры.

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

Надежная последовательность выглядит так:

  1. Выберите возможность с одним вероятным владельцем и допустимой границей согласованности. Учтите всех читателей и писателей ее таблиц.
  2. Зафиксируйте текущее поведение характеризационными тестами, включая ошибки, повторы, округление, null и операторские исправления. Записывайте типичные производственные запросы только по правилам конфиденциальности организации.
  3. Введите будущую схему и заполните ее повторяемыми заданиями с контрольными точками. Читайте обе версии в теневом режиме и сравнивайте, не отдавая новый результат.
  4. Перейдите к одному полномочному пути записи. Если временная двойная запись неизбежна, журналируйте оба результата и запускайте сверку; не предполагайте, что независимые записи останутся равны.
  5. Переведите чтение, а затем трафик к новому владельцу, сохраните проверенный откат и удаляйте старые права лишь после устойчивой задержки и паритета в согласованных пределах.

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

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

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

Тесты паритета должны сравнивать бизнес-эффекты

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

Создайте стенд паритета, который отправляет один записанный или синтетический запрос старому и новому пути, нормализует допустимые различия и сравнивает ответы вместе с эффектами в базе. Для команды сравнивайте строки, деньги, события и классы ошибок. Для запроса проверяйте порядок, страницы, null и фильтры доступа. Маскируйте чувствительные значения до стенда и сохраняйте результат как доказательство миграции.

Задавайте допуски по полям. Временные метки могут различаться в заданном окне. Сгенерированные ID могут не совпасть, но должны сохранять связи. Денежные десятичные значения обычно обязаны совпадать после объявленного округления. Сплошной JSON diff создает шум, пока инженеры не начинают его игнорировать.

Проверяйте сбои. Остановите relay после публикации, повторите запрос с таймаутом, доставьте события не по порядку, восстановите снимок и запустите старого потребителя с новой версией события. Граница убедительна, когда сбои имеют ограниченные последствия и описанный ремонт, а не когда диаграмма выглядит аккуратно.

Здесь уместен подход CodeHero: платформа читает весь код, обновляет архитектуру и сверяет поведение с записанным производственным трафиком на стенде паритета. Обещание выполнить работу меньше чем за 30 дней было бы безрассудным, если бы эффекты в базе не считались поведением, а исходные файлы просто переводились по одному.

Совместимые изменения дают независимое развертывание

Сделайте разделение схемы убедительным
CodeHero обновляет архитектуру и сравнивает бизнес-эффекты с производственным трафиком.

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

Применяйте expand and contract для полей через границу. Сначала добавьте новое поле, не удаляя старое. Научите писателей заполнять новое представление, внесите исторические строки и дайте читателям предпочитать новое, принимая старое. Убедитесь, что старых читателей нет, прекратите выпуск старого поля и удалите его позже. Каждой фазе нужен измеримый критерий выхода, например ноль чтений старого столбца за полный рабочий цикл, а не дата на глаз.

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

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

Представления базы дают короткое окно совместимости для переименованного чтения. Они плохо работают как постоянный API, когда потребители зависят от недокументированных join или планов запросов. Задайте условие удаления каждого представления и наблюдайте за обращениями. Поиск по репозиторию не найдет разовые отчеты и бинарные файлы вне основной сборки.

Откат проверяет честность плана. Если новое приложение пишет значение, которое старая версия не читает, возврат бинарного файла не восстановит сервис. Проверяйте старый код на состоянии нового, включая enum, null, длинные строки и варианты событий. Обратно совместимый DDL решает половину задачи; сохраненные данные должны читаться во всем окне отката.

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

Некоторым монолитам стоит остаться монолитами

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

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

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

До одобрения разделения требуйте короткую запись с собственными таблицами, полномочным писателем, чтением через границу, окном согласованности, контрактом событий, единицей восстановления, порядком миграции и владельцем отката. Отклоняйте ответы оба сервиса или eventually без предела и ремонта. Такой стандарт предотвращает больше ущерба, чем спор об идеальном числе сервисов.

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

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

Вопросы

Нужна ли каждому микросервису отдельная база данных?

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

Всегда ли общая база вредна для микросервисов?

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

Как найти границы транзакций в монолите?

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

Может ли API убрать связанность через базу?

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

Когда стоит применять eventual consistency?

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

Как безопаснее всего разделить общую таблицу?

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

Решает ли брокер распределенные транзакции?

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

Как строить отчеты после разделения монолита?

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

Что должен включать откат микросервиса?

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

Модульный монолит лучше микросервисов?

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