Игорь_Прагматик
2026-08-02 00:06
Ну давай разберем. Все в последние годы кинулись в CQRS как в моду, но половина этих реализаций — это карго-культ: навесили шину событий, наклепали саг, а производительность просела так, что проще было бы старый добрый CRUD оставить. Я за 12 лет насмотрелся на такие «архитектурные шедевры» и теперь предпочитаю CQRS без централизованной шины событий. Давайте разберем, почему это работает и когда это разумно.
Сначала про боль. Event Bus — это здорово, когда у вас микросервисы, несколько команд и нужна асинхронность. Но в монолите, который чаще всего и встречается, шина превращается в болото: события летают туда-сюда, обработчики дергают друг друга, и вы получаете распределенный монолит с задержками и невозможностью отладки. Я как-то раз потратил две недели на трассировку одного события, которое порождало цепочку из пяти обработчиков, и в итоге оказалось, что проблема была в неправильном порядке саг. Это не архитектура, это стройка без проекта.
Что я делаю вместо этого? Внутри монолита я разделяю команды и запросы на уровне интерфейсов, но события передаю явными вызовами через DI-контейнер. То есть, если команда изменяет состояние, она просто вызывает нужный обработчик синхронно, без посредников. Да, это связывает компоненты, но в монолите это нормально — вы всегда можете заменить синхронный вызов на очередь позже, если реально упретесь в нагрузку. Зато у вас нет магии, все вызовы видны в коде, и дебажить такое — одно удовольствие.
Пример из практики: у нас был заказ, который при создании должен был обновить склад, отправить уведомление и сгенерить счет. С шиной это выглядело как три события, каждое со своим обработчиком, и половина времени уходила на гарантии доставки. Я заменил это на три прямых вызова в одном сервисе, обернутых в транзакцию. Результат: время выполнения упало с 800 мс до 150 мс, а код стал понятнее — последовательность действий читается как инструкция.
Конечно, есть случаи, когда шина оправдана: если у вас реально несколько сервисов, или события нужны внешним системам, или требуется строгая асинхронность для отказоустойчивости. Но это редкость. Я бы советовал правило: если вы не можете объяснить, зачем вам шина, кроме «так модно», — не ставьте ее. Начните с простого CQRS, где команды синхронно меняют состояние, а запросы идут через отдельные read-модели, и только потом, если появится реальная потребность в асинхронности, выносите это на брокер.
Еще момент: read-модели. Многие забывают, что CQRS — это в первую очередь разделение чтения и записи, а не события. Я часто вижу, как команда пишет в одну БД, а чтение делает из той же таблицы через те же сущности — и это не CQRS, это просто рефакторинг. Нормальный подход: для запросов сделать отдельные проекции, обновляемые после команд через тот же синхронный вызов. Это просто, быстро и не требует шины. У меня на одном проекте так read-модели обновлялись за 10 мс, и нагрузку держали без проблем.
В итоге мой совет: не гонитесь за хайпом. CQRS — это про разделение ответственности, а не про инфраструктуру. Начните с минимального решения — явные вызовы, отдельные read-модели, и только когда упретесь в масштабирование, добавляйте брокер. Это сэкономит вам кучу нервов и денег на серверах. И да, если кто-то скажет, что без шины это не «истинный» CQRS, спросите его, сколько раз он ночами разгребало потерянные события. Спойлер: я такие ночи провел достаточно.