Как стать автором
Обновить
483.9
OTUS
Цифровые навыки от ведущих экспертов

Подумайте перед тем, как внедрять CQRS

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров2.5K

Привет, Хабр!

Если вы тимлид или архитектор, и в команде всё чаще звучит «давай сделаем CQRS» — стоит остановиться. Этот паттерн мощный, но далеко не беспроблемный.

Зачем вообще вспоминать о CQRS, если есть Entity Framework и SaveChanges()?

Проблема — в нагрузке. Любой монолит когда‑нибудь упирается в диск/CPU/команду DevOps. CQRS предлагает разделить модели чтения и записи, оптимизируя их независимо. В теории ─ вроде ок:

  • Write‑модель заточена под транзакционную целостность, минимум индексов, максимум through‑put.

  • Read‑модель ломится от индексов, денормализации и кэширования.

Microsoft рисует это на своих красивых диаграммах уже много лет. Но цена вопроса — сложность.

Лишний оверхед для банального CRUD

У нас простая табличка Customers, может просто сделать DbSet<Customer>? В CQRS на один такой Customer внезапно появляется:

Слой

Что появляется

Команды

CreateCustomerCommand, UpdateCustomerCommand, …

Обработчики

CreateCustomerHandler, UpdateCustomerHandler, …

DTO

CustomerDto, CustomerDetailsVm, …

Маппинг

AutoMapper‑профили туда‑сюда

Тесты

минимально ×2: на write и на read

Это шесть файлов вместо одной сущности. И пока вы смотрите на pull‑request, бизнес ждёт фичу. Именно поэтому на StackOverflow плакали, что CQRS избыточен в data‑heavy формах

Пример:

Без CQRS

public async Task<IActionResult> Post(CustomerDto dto) {
    var entity = _mapper.Map<Customer>(dto);
    _db.Customers.Add(entity);
    await _db.SaveChangesAsync();
    return Ok();
}

С CQRS + MediatR

// 1. Command
public record CreateCustomerCommand(string Name, string Email) : IRequest<Guid>;

// 2. Handler
public class CreateCustomerHandler : IRequestHandler<CreateCustomerCommand, Guid>
{
    private readonly AppDbContext _db;
    public async Task<Guid> Handle(CreateCustomerCommand cmd, CancellationToken ct)
    {
        var entity = new Customer { Name = cmd.Name, Email = cmd.Email };
        _db.Customers.Add(entity);
        await _db.SaveChangesAsync(ct);
        return entity.Id;
    }
}

Добавьте к этому проектирование read‑модели, и бонусом — задержка, о которой ниже.

Если у вас нет явной причины (трафик на чтение > трафика на запись, микросервис живёт автономно, нужна денормализация), не трогайте CQRS.

Конечная согласованность

Есть кейс в Amazon, когда товар положили в корзину, а при оплате корзина пуста.

Причина банальна: write‑модель отстрелялась (команда выполнена), а read‑модель ещё не проиндексировала событие.

Как воспроизвести баг локально

  1. Создаём команду AddToCart.

  2. В CartReadModel слушаем ProductAdded и апдейтим denormalized view.

  3. Перед запуском параллельно стреляем 1000 команд.

  4. Между AddToCart и GET /cart — 100 мс. Часть запросов вернёт пустую корзину.

// псевдокод
Parallel.For(0, 1000, _ =>
{
    _bus.Send(new AddToCart(userId, productId));
    var cart = _api.GetCart(userId); // шанс увидеть прошлую версию
});

Смягчаем боль

  • Read‑your‑write: после команды делаем прямой запрос к write‑storage (Postgres → RETURNING)

  • Versioning/ETag: фронт ждёт, пока cart.versioncommandVersion.

  • Outbox Pattern: событие публикуется только после коммита транзакции; гарантирует, что read‑проекция получит все ивенты в нужном порядке.

  • Пессимистичный UX: disable кнопка «Pay» до подтверждения синхронного RPC.

Частные vs публичные данные

Делите данные на private draft и public published. В системах управления контентом это очевидно («Черновик» и «Опубликовать»), но для интернет‑магазина звучит непривычно: «Корзина — приватная, Заказ — публичный».

Пока объект живёт в приватной зоне, вы можете мириться с eventual consistency, потому что видит его ровно один пользователь. Когда же объект становится публичным (товар появился в «Рекомендациях»), лаг превращается в расхождение ценников у разных юзеров.

Отсюда простое правило:

Плохой API
POST /products    // сразу публично

Хороший API
POST /products/drafts
POST /products/{id}/publish   // явный переход

Так избегаем неожиданного всплытия неподготовленных данных и сигнализируем бизнесу момент потери контроля.

Масштабирование: когда read-модель не успевает за write-моделью

При пиковых нагрузках очередь событий обгоняет проектор. Read‑модель отстаёт на секунды, минуты, часы. На InfoQ об этом писали — «Day Two Problems».

Вертикальный срез против традиционных слоёв

Строим фичу как вертикальный срез — от контроллера до БД — и тестируем её изолированно.

На практике вертикальный срез + CQRS выглядит так:

/Features
  /Orders
    Create.cs
    CreateValidator.cs
    CreateHandler.cs
    OrderReadModel.cs

Каждая папка — мини‑резерт. Хотите масштабировать? Деплойте /Orders отдельно, а Users — в другом контейнере. Read‑проекции отделяются физически, и очередь перестаёт быть единой «точкой плавления».

Качество данных: «грязные» события и миграции

События — это историческая правда. Но если их схема меняется каждые две недели, любая read‑модель будет сыпаться, пока вы догоняете новые поля.

Для этого делаем:

  1. Immutable events: новые поля — новый тип события (ProductPriceChangedV2).

  2. Upcasters: при чтении старых событий приводим к новой форме.

  3. Golden tests: фиксация payload‑ов в репозитории (snapshot‑тесты), чтобы случайно не сломать back‑compat.

Когда CQRS стоит того

Симптом

CQRS спасает?

90% запросов — чтение

Да

Нужно линейно масштабировать чтение

Да

Простой CRUD без отчётов

Нет

Транзакция должна обновить сразу 5 таблиц

Скорее нет

Домена сложная, бизнес‑инvariants тяжёлые

Часто да

Вывод

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

Полезно еще почитать материал по этой ссылке.


Как архитектор или тимлид, вы понимаете, насколько важно развивать команду. OTUS предлагает курсы для специалистов в ключевых IT-направлениях — от DevOps до программирования и аналитики. Это гибкие форматы обучения с акцентом на практику, которые легко интегрируются в рабочий процесс. Узнайте, как OTUS поможет повысить квалификацию вашей команды и улучшить IT-процессы в компании.

Теги:
Хабы:
0
Комментарии5

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS