#system_design
Разбавим
Python-посты архитектурой!На днях со студентами System Design World обсуждали паттерны, и закономерно всплыл CQRS.
Его просто обожают на System Design Interview, и... регулярно путают с CQS!
Micro vs Macro
CQS (Command-Query Separation) - это принцип создания классов и API.
- Command-методы меняют состояние и либо НЕ отдают данные (
void), либо возвращают служебные значения (id, ok, error и т.д.);- Query-методы возвращают данные и никогда НЕ меняют состояние.
На простых классах от CQS мало пользы, зато при написании фабрик, репозиториев и прочих паттернов он реально выручает:
- меньше неявного поведения;
- проще кэшировать и оптимизировать;
- проще поддерживать и дебажить код.
Пример:
from dataclasses import dataclass
@dataclass(frozen=True)
class Car:
num: str
class Base:
def __init__(self):
self._cs = {}
class FactoryBad(Base):
# get with unexpected side effect
def get(self, num: str) -> Car:
if num not in self._cs:
self._cs[num] = Car(num=num)
return self._cs[num]
class FactoryCQS(Base):
def find(self, num: str) -> Car | None:
return self._cs.get(num)
def get(self, num: str) -> Car:
return self._cs[num]
def register(self, num: str) -> None:
if num in self._cs:
raise ValueError(f"Car with number '{num}' already exists!")
self._cs[num] = Car(num=num)
CQRS (Command Query Responsibility Segregation) - это архитектурный паттерн:
- разные пути и модели данных для записи и чтения - write-side и read-side;
- часто разные Handler'ы, контракты и схемы - но всё же это детали реализации.
По сути CQRS - это CQS «на стероидах».
CQRS - Кафка, Стриминг, 2 Сурса
Частая ошибка - воспринимать CQRS как обязательную связку из условных read- и write-СУБД, очереди и Eventual Consistency.
Действительно, так часто бывает, но в первую очередь CQRS - про разделение путей и моделей данных, а не про инфраструктуру.
Начать внедрение CQRS можно и с 1 СУБД:
- write - нормализованные таблицы под базовые сущности;
- read - денормализованные таблицы/
view под чтение;- если обновлять read-проекции синхронно с write-проекциями, можно получить и Strong Consistency.
Зачем всё это
CRUD-сервис «на всё» (
create, update, get, find, ...) быстро «пухнет»:- чтение и запись смешиваются, API становится неочевидным;
- хотим масштабировать чтение, но read-only инстансы вынужденно тащат write-зависимости и флаги/роутинг;
- репозиторий превращается в комбайн с бесконечными зависимостями;
- страдает производительность;
- сложнее растить команду.
CQRS предлагает решение этой проблемы:
- изолированные Handler'ы для команд и запросов (часто реально «по 1 файлу на операцию»);
- лишь нужные зависимости в каждом Handler'е - ускоряет разработку и реально отделяет write-side от read-side.
CQRS - не серебряная пуля:
- если проект компактный и несложный - лучше CRUD + нормальный репозиторий;
- CQRS добавляет бойлерплейт. Даже если код «генерится Claude'ом», растет объём и контекст.
Идемпотентность команд - must have
Команды могут повторяться из-за retry, timeout и at-least-once доставки. Handler должен быть идемпотентным и не допускать создания дубликатов.
Блеск CQRS
CQRS раскрывается, когда система становится read-heavy/нуждается в разных формах данных. Тогда вы:
- масштабируете read- и write-side независимо;
- держите read-модели под конкретные задачи: поиск, отчёты и т.д.
Отдельный плюс - несколько read-моделей одновременно:
Postgres для запросов, Elastic для FTS и т.д. При этом они могут строить свои проекции из единого потока событий (event bus, outbox, CDC и т.д.). Отсюда и дружба с Event Sourcing: при хранении изменений как Event Log, проекции можно пересобирать с нуля.Идеи CQS и CQRS во многом звучат как «нормально делай - нормально будет», они очень интуитивны. Тем не менее выработка общих терминологии и понимания - это всегда большой плюс.
В следующий раз разберём Event Sourcing!
С уважением,
Михаил Масягин
P.S. Рекомендую к просмотру выступление Андрея Цветциха.
