В приложении модель заказа разделили на запись и чтение:
module ЗаказыЗапись {
function изменитьАдрес(id, адрес) {
сохранитьЗаказ(id, адрес)
опубликовать(АдресЗаказаИзменён(id, адрес))
}
}
module ЗаказыЧтение {
function найтиДляЭкрана(id) {
return readModel.find(id)
}
}
Какой главный архитектурный эффект даёт такое разделение?
Разделение записи и чтения позволяет независимо развивать и оптимизировать модели для изменения состояния и его представления. Это уменьшает связанность сложной бизнес-логики с требованиями экранов и отчётов, но обычно вводит eventual consistency, дублирование данных и необходимость синхронизации read model.
Подход CQRS появился как реакция на системы, где одна универсальная модель одновременно обслуживала команды, запросы, отчёты и разные пользовательские представления. Такая модель постепенно обрастала специальными полями и обходными правилами: оптимизация чтения начинала усложнять изменение бизнес-состояния.
Разделение command model и query model позволяет применять к ним разные структуры хранения и правила эволюции. Это не обязательное свойство микросервисов и не требование использовать события: CQRS может применяться внутри одного приложения.
Модель записи отвечает за инварианты и изменение состояния, а модель чтения — за быстрый и удобный вывод данных. Если заставить одну модель хорошо решать обе задачи, изменения формата экрана или отчёта могут затронуть бизнес-логику и её хранилище.
При разделении появляется новая граница согласованности. После успешной команды read model может обновиться не мгновенно, поэтому пользователь иногда увидит прежнее состояние. Неверное решение — скрыть эту задержку обещанием синхронной актуальности там, где система её не гарантирует.
Команда проходит через write model, которая проверяет права, инварианты и изменяет источник истины. Затем система публикует факт изменения, например событие АдресЗаказаИзменён. Обработчик события обновляет одну или несколько специализированных read model.
Запросы обращаются к read model и не должны изменять бизнес-состояние. Её можно денормализовать, снабдить индексами или хранить в другой базе, если это упрощает конкретные сценарии чтения.
Ключевой эффект — независимая эволюция моделей. Добавление отчётного поля может потребовать изменения проекции, но не обязано усложнять агрегат, который отвечает за правила изменения заказа.
Цена этого решения — асинхронная синхронизация, повторная обработка событий, контроль версии схемы событий и восстановление проекций. Обработчики должны быть устойчивы к повторам, а система — иметь стратегию исправления отставшей или повреждённой read model.
CQRS не оправдан, если доменная модель проста, чтение и запись используют почти одинаковую структуру, а задержка согласованности недопустима. В таких случаях разделение добавит инфраструктуру и операционные риски без существенной пользы.
Здесь writeStore остаётся источником истины, а readStore — производным представлением. Если обработчик не успел выполниться, команда всё равно могла завершиться успешно, но запрос временно вернёт старые данные.
В интернет-магазине оформление заказа имело строгие правила: резервирование товара, проверку лимита и расчёт итоговой суммы. Одновременно аналитики требовали десятки вариантов отчётов с фильтрами по складу, акциям и каналам продаж. Попытка строить отчёты непосредственно по транзакционной модели привела к тяжёлым запросам и усложнению схемы заказа.
Рассматривались три варианта. Первый — оставить единую модель и добавлять индексы: это проще в эксплуатации, но связывает отчёты с доменными таблицами. Второй — вынести отчёты в отдельную базу через периодические выгрузки: снижает нагрузку, однако увеличивает задержку и усложняет полноту данных. Третий — построить отдельные проекции из событий: он обеспечивает специализированное чтение и независимую эволюцию, но требует повторной обработки событий и мониторинга отставания.
Выбрали третий вариант для аналитических сценариев, сохранив синхронную модель записи для оформления заказа. Пользователю явно показывали время последнего обновления отчёта, а для операций, требующих немедленной точности, обращались к write model. В результате отчётные запросы перестали конкурировать с транзакциями, но команда отдельно приняла и контролировала стоимость eventual consistency.
Нет. CQRS разделяет модели команд и запросов, а event sourcing меняет способ хранения состояния: источником истины становятся события, из которых состояние восстанавливается. Их можно сочетать, но можно реализовать CQRS с обычным текущим состоянием в write model и событиями только для обновления проекций.
Проекция может стать неполной или некорректной. Поэтому нужны идентификаторы событий, версии агрегата или последовательности, политика повторной доставки, идемпотентная обработка и возможность полностью пересоздать read model из надёжного источника событий или журнала изменений.
Порядок следует требовать только там, где он семантически необходим. Глобальный порядок для всех событий обычно дороже и ограничивает масштабирование; часто достаточно порядка внутри одного агрегата или ключа.
Полной гарантии через асинхронную проекцию нет: сразу после команды запрос может попасть на устаревшую read model. Возможные решения — вернуть результат команды напрямую, читать критичный сценарий из write model, ждать обработки события до заданной точки или передавать клиенту версию, до которой чтение должно быть доведено.
Каждый вариант имеет цену. Синхронное ожидание уменьшает видимую задержку согласованности, но возвращает связанность и увеличивает время ответа; чтение из write model сохраняет точность, но может быть медленнее и хуже масштабироваться.