CQRS pattern: Command side, Event Store, Kafka event bus, Read-side projections, cache, event replay
Event Sourcing меняет главный вопрос хранения данных. Вместо «какое текущее состояние объекта?» система хранит «какие факты привели объект в это состояние?». Баланс счёта — не просто число 500, а последовательность событий: счёт открыт, пополнение 200, пополнение 400, списание 100. Текущее состояние выводится replay-ем событий, а не является единственным источником правды.
Это нужно там, где история сама по себе ценна: деньги, заказы, медицинские записи, государственные процессы, биллинг, аудит, расследования, временные запросы «что мы знали на тот момент?». Event Sourcing также полезен, когда из одной истории нужно строить несколько read models: карточку заказа, timeline, fraud alerts, аналитику, уведомления. Поэтому паттерн естественно сочетается с CQRS: write side принимает commands и append-ит events, read side строит projections.
Но Event Sourcing не является «продвинутым CRUD». Он усложняет мышление команды. События immutable, схемы событий живут годами, replay должен работать на старых данных, а read consistency становится eventual. Если эти свойства не дают бизнес-выгоды, паттерн легко становится дорогой игрушкой.
Event Store — это журнал фактов. Aggregate загружается не из строки orders, а из потока событий orders-ord_7723: OrderPlaced, PaymentAuthorized, OrderShipped. Command handler применяет команду к текущему состоянию aggregate, проверяет инварианты и append-ит новое событие. Projection читает события и строит удобные read views.
Состояние — derived view. Его можно пересчитать. Если изменилась схема read model, можно replay-нуть историю и построить новую projection. Если нужен новый отчёт, не обязательно менять write model: можно добавить ещё один consumer событий. Цена этой силы — дисциплина: события должны быть фактами прошлого, а не командами в будущем; они должны быть версионированы; replay должен быть детерминированным.
CQRS здесь не украшение. Без отдельной read side запросы к Event Store часто будут медленными и неудобными. Event Store хорош для append и replay, но не для произвольных UI-запросов. Поэтому production Event Sourcing почти всегда сопровождается projections, cache и отдельной query API.
Диаграмма показывает command side, query API, event store, snapshot store, Kafka-like event bus, schema registry и read-side projections. Command API принимает команду PlaceOrder. Command Handler десериализует payload, Validator проверяет business rules: inventory, pricing, user limits. После этого handler append-ит OrderPlaced в Event Store DB, обновляет snapshot для aggregate и публикует событие в Kafka Broker.
Schema Registry на диаграмме подчёркивает, что события являются контрактом. Нельзя бездумно менять payload события, потому что старые consumers и replay будут читать старые версии. Orders Projection и Analytics Projection подписаны на event bus и материализуют разные представления в Read PostgreSQL. Read API сначала смотрит в Redis Cache, затем падает back to projection DB и populates cache.
Отдельный сценарий replay показывает важнейшую способность Event Sourcing: можно инициировать rebuild projections, прочитать события с начала или с snapshot position и заново заполнить read side. Это не миграционная магия, а ожидаемая операция, которую нужно проектировать и регулярно проверять.
Place Order Command учит, что событие появляется только после проверки инвариантов. Command PlaceOrder не является фактом; это намерение. Фактом становится OrderPlaced, когда validator подтвердил inventory, pricing и limits, а event append успешно прошёл. ADR-решение: write side хранит только валидные domain facts. Альтернатива — складывать все команды в лог и разбираться позже — часто усложняет бизнес-инварианты и аудит.
Query Order показывает CQRS-сторону Event Sourcing. Read API не replay-ит stream заказа на каждый GET. Он читает cache или projection DB, где уже лежит материализованная форма. Это учит не путать источник правды и query storage. Event Store отвечает за историю, projection DB отвечает за скорость чтения.
Replay Events показывает восстановление derived state. Event Store отправляет события пачками через event bus, projections заново делают upsert в read DB, snapshot positions обновляются. Этот сценарий учит проектировать replay как штатную возможность: batch size, idempotency, ordering, checkpointing, monitoring, backpressure. Если replay ломается на событии трёхлетней давности, проблема не в старом событии, а в том, что система потеряла совместимость.
Также диаграмма косвенно учит роли snapshots. Если aggregate имеет сотни тысяч событий, каждый command не должен replay-ить всю историю с нуля. Snapshot store хранит checkpoint состояния, а handler догоняет только хвост событий после snapshot. Snapshot не заменяет event log, а ускоряет восстановление текущего состояния.
ADR: хранить state или events? Контекст: бизнес требует audit trail, temporal queries и несколько projections. State-based CRUD проще: одна таблица хранит текущую правду, миграции привычны, queries понятны. Event Sourcing сложнее: нужны event streams, versions, replay, projections. Решение в пользу Event Sourcing оправдано, когда история является бизнес-активом, а не побочным логом.
ADR: Event StoreDB, Kafka или Postgres? EventStoreDB даёт purpose-built streams, optimistic concurrency и subscriptions. Kafka хорош как distributed log и backbone для stream processing, но aggregate-level consistency придётся проектировать. Postgres + append-only table + outbox проще для небольших систем и команд, которым важна транзакционность в одной БД. Выбор зависит от масштаба, команды и требований к ordering.
ADR: snapshot frequency. Частые snapshots ускоряют command handling, но увеличивают write amplification и усложняют формат snapshot-а. Редкие snapshots проще, но replay aggregate-а может стать медленным. Решение обычно привязывают к размеру stream-а или времени восстановления: snapshot каждые N событий или когда replay превышает заданный latency budget.
ADR: synchronous projection или eventual read side. Синхронная projection уменьшает stale reads, но связывает write availability с read storage и превращает append в многокомпонентную операцию. Асинхронная projection сохраняет чистый event log и независимое масштабирование, но требует UX и API вокруг lag. В Event Sourcing чаще выбирают асинхронность и явно показывают status: command accepted, projection catches up.
ADR: immutable events и schema evolution. Нельзя «поправить старое событие» как строку таблицы, не разрушив аудит. Вместо этого применяют новые версии событий, upcasters, backward-compatible schemas и компенсирующие events. Это дороже, но сохраняет доверие к истории.
EventStoreDB является специализированной платформой для Event Sourcing. Axon Framework и Marten дают Event Sourcing/CQRS для JVM и .NET/Postgres экосистем. Kafka часто используется как event log и delivery backbone, особенно в data-heavy системах. DynamoDB Streams и Lambda дают serverless-вариант для projections. В банковских системах event log полезен для ledger-а и расследований. В e-commerce история заказа естественно выражается событиями: placed, paid, packed, shipped, returned.
Внутри крупных продуктов Event Sourcing часто применяется не ко всему, а к bounded contexts: платежи, биллинг, inventory, audit trail. Остальные части остаются CRUD. Это зрелый компромисс: не нужно превращать всю компанию в event-sourced архитектуру, чтобы получить пользу там, где история действительно важна.
CloudArch-диаграмма близка к cqrs, но акцент другой. В cqrs главный вопрос — разделение write/read моделей. Здесь главный вопрос — event log как источник правды и ability to replay. Для надёжной публикации событий рядом стоит изучить transactional-outbox; для long-running бизнес-процессов — saga-choreography; для ошибок потребления событий — dead-letter-queue.
Первая ошибка — Event Sourcing для простого CRUD. Если пользователю нужна только последняя версия профиля, а аудит не важен, event streams добавят сложность без отдачи.
Вторая ошибка — события как команды. SendEmailRequested может быть событием только если факт действительно произошёл в домене; иначе это command, который ещё может быть отклонён. События должны называться в прошедшем времени и выражать факт.
Третья ошибка — бизнес-логика в projections. Projection должна строить read model, а не решать, можно ли выполнить списание или отмену. Инварианты живут в aggregate/command handler.
Четвёртая ошибка — отсутствие upcasters и версии схемы. Старые события будут жить годами. Если код умеет читать только новую форму payload, replay станет невозможным.
Пятая ошибка — replay без идемпотентности. Projection может получить событие повторно после retry или rebuild. Upsert, processed event ids, deterministic handlers и checkpointing должны быть частью дизайна.
Шестая ошибка — использовать event log как универсальную аналитическую БД. Event Store хранит факты и порядок, но аналитические запросы должны идти в projections: ClickHouse, Pinot, Postgres views или другое специализированное storage.
Не используйте Event Sourcing, если история не нужна, команда не готова владеть event schemas, а бизнес не принимает eventual consistency. Не используйте его как способ «получить аудит бесплатно»: аудит требует неизменяемости, доступа, retention, redaction policy и понятного semantic model событий.
Паттерн плохо подходит для доменов, где state часто переписывается без ценности истории, а queries ad-hoc и непредсказуемы. Он также опасен, если compliance требует физически удалить персональные данные: нужно заранее проектировать crypto-shredding, tombstone events или разделение PII и domain events.
Не стоит начинать с полной платформы Event Sourcing для всего монолита. Лучше выбрать bounded context с явной пользой: ledger, order lifecycle, subscription billing, inventory movements.
cqrs — разделение command и query responsibilities.transactional-outbox — атомарная запись события и последующая публикация.message-queues — delivery semantics для event bus.dead-letter-queue — обработка poison events и replay после фикса.saga-choreography — long-running процессы поверх domain events.