CQRS: разделяем чтение и запись без лишнего пафоса

Что такое CQRS, зачем разделять модели чтения и записи, как это связано с event sourcing и где этот паттерн создаёт больше проблем, чем решает

CQRS (Command Query Responsibility Segregation) — один из тех паттернов, которые часто описывают как обязательный спутник event sourcing и микросервисов. На практике это самостоятельный принцип, который может быть полезен и в монолите, и без event sourcing, и даже без отдельных баз данных.

Суть проста: модель для записи (commands) и модель для чтения (queries) — это разные модели. Они могут жить в одной базе, но обслуживают разные потребности и оптимизированы под разные сценарии.

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

В статье

Что такое CQRS и чем он не является

CQRS разделяет операции на два типа:

  • Command — намерение изменить состояние (AllocateReserve, ConsumeResource). Может быть отклонена. Не возвращает данные (или возвращает только ID/статус).
  • Query — запрос данных (GetCurrentResourceLevel, ListRecentConsumption). Не меняет состояние. Идемпотентна.

Это не означает:

  • две обязательные базы данных;
  • обязательный event sourcing;
  • обязательную асинхронность;
  • обязательные микросервисы.

CQRS — это принцип проектирования, а не конкретная инфраструктура.

Три уровня разделения

Уровень 1: разделение интерфейсов

Самый лёгкий вариант. Одна БД, но API разделён: command-хендлеры и query-хендлеры — разные модули. Write-модель и read-модель — разные структуры в коде, даже если обращаются к одним таблицам.

Польза: код становится чище, обязанности не смешиваются. Стоимость: почти нулевая.

Уровень 2: разные модели, одно хранилище

Write-модель нормализована и оптимизирована для консистентности. Read-модель — денормализована, оптимизирована для быстрых запросов (materialized views, кеши). Обе живут в одной базе или кластере.

Польза: чтение ускоряется без усложнения write-пути. Стоимость: поддержка синхронизации между моделями.

Уровень 3: полное разделение хранилищ

Write и read — в разных базах. Синхронизация через события (часто вместе с event sourcing). Read-хранилище может быть PostgreSQL, Elasticsearch, Redis — что подходит под сценарий.

Польза: независимое масштабирование, оптимальные хранилища под каждую задачу. Стоимость: eventual consistency, сложность инфраструктуры, дополнительный код.

CQRS + Event Sourcing: естественная, но не обязательная связка

CQRS и event sourcing часто упоминают вместе, потому что event sourcing естественно создаёт разделение: события — это write-сторона, проекции — read-сторона.

Но:

  • CQRS без event sourcing — нормально. Разделение моделей полезно и с обычными таблицами.
  • Event sourcing без CQRS — возможно, но неудобно. Без отдельной read-модели придётся каждый раз replay’ить события.

Связка мощная, но если задача не требует event sourcing — CQRS можно применять изолированно.

Когда CQRS оправдан

  • Сильная асимметрия нагрузки — чтений в 100× больше, чем записей (или наоборот);
  • разные требования к моделям — write-модель должна быть строго нормализованной, read-модель — денормализованной для быстрых дашбордов;
  • несколько представлений одних данных — одна и та же сущность нужна в разных форматах для разных потребителей;
  • сложная доменная логика на записи — command-сторона содержит валидацию, бизнес-правила, инварианты, которые не нужны при чтении;
  • независимое масштабирование — read-реплики без нагрузки на write-путь.

Где CQRS создаёт лишнюю сложность

  • Простой CRUD — если read и write модели почти идентичны, разделение добавляет код без пользы;
  • строгая консистентность на чтение — если пользователь должен сразу видеть свои изменения, eventual consistency вызовет проблемы;
  • маленькая команда — поддержка двух моделей, синхронизации и мониторинга требует ресурсов;
  • нет метрик — без понимания профиля нагрузки (read/write ratio, latency requirements) решение о разделении принимается наугад.

Лунная база: зачем разделять модель управления и мониторинга

В кейсе лунной базы CQRS возникает естественно:

Write-сторона (управление):

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

Read-сторона (мониторинг):

  • текущее состояние всех подсистем — дашборд;
  • история расхода ресурсов — графики;
  • прогноз исчерпания — аналитическая модель;
  • аварийные оповещения — real-time.

Каждый read-сценарий требует своей оптимизации. Пытаться обслужить их все из одной нормализованной модели — путь к медленным запросам и сложным JOIN’ам.

Eventual consistency: с чем придётся жить

При полном разделении хранилищ (уровень 3) с асинхронной синхронизацией read-модель отстаёт от write-модели на время распространения изменений. Это не свойство любого раздельного хранилища как такового — при синхронном обновлении отставания нет; но именно асинхронный вариант обычно и выбирают ради масштабирования и устойчивости к сбоям. Это значит:

  • пользователь может не сразу увидеть свои изменения;
  • разные read-модели могут показывать разные состояния в один момент времени;
  • нужен мониторинг лага между write и read;
  • в критичных сценариях может потребоваться «read your own writes» — чтение из write-модели после собственной команды.

Eventual consistency — не баг, а trade-off. Если он неприемлем для конкретного сценария, используйте уровень 1 или 2.

Checklist: нужен ли вам CQRS

  1. Сильно ли отличаются модели чтения и записи в вашем домене?
  2. Есть ли асимметрия нагрузки (read-heavy или write-heavy)?
  3. Нужны ли несколько разных представлений одних данных?
  4. Готова ли команда работать с eventual consistency (если уровень 3)?
  5. Есть ли метрики, подтверждающие узкое место на чтении или записи?
  6. Достаточно ли простого разделения интерфейсов (уровень 1) вместо полного разделения?

Начинайте с уровня 1. Переходите дальше только когда появляется измеримая причина.

Документация и первоисточники

Обсуждение в Telegram

Присоединиться →

Комментарии