GH GambleHub

CQRS і розділення читання/запису

Що таке CQRS

CQRS (Command Query Responsibility Segregation) - архітектурний підхід, який розділяє модель даних і компоненти, що відповідають за запис (commands) і читання (queries).
Ідея: процес зміни стану оптимізується під валідні інваріанти і транзакції, а читання - під швидкі, цільові проекції і масштабування.

💡 Команди змінюють стан і повертають результат операції. Запити тільки читають і не мають побічних ефектів.

Навіщо це потрібно

Продуктивність читання: матеріалізовані проекції під конкретні сценарії (стрічки, звіти, каталоги).
Стабільність критичного шляху: запис ізольований від «важких» джоїнів і агрегатів.
Свобода вибору сховищ: OLTP для запису, OLAP/кеш/пошукові рушії для читання.
Прискорена еволюція: додавайте нові уявлення без ризику «зламати» транзакції.
Спостережуваність і аудит (особливо в зв'язці з Event Sourcing): легше відновити і переграти стан.


Коли застосовувати (і коли ні)

Підходить, якщо:
  • Превалюють читання з різними зрізами даних і складною агрегацією.
  • Критичний шлях запису повинен бути тонким і передбачуваним.
  • Потрібні різні SLO/SLA для читання і запису.
  • Потрібна ізоляція доменної логіки запису від аналітичних/пошукових потреб.
Не підходить, якщо:
  • Домен простий, навантаження невисоке; CRUD справляється.
  • Сильна узгодженість між читанням і записом обов'язкова для всіх сценаріїв.
  • Команда недосвідчена, а операційна складність неприйнятна.

Базові поняття

Команда (Command): намір змінити стан ('CreateOrder','CapturePayment'). Перевіряє інваріанти.
Запит (Query): отримання даних ('GetOrderById','ListUserTransactions'). Без побічних ефектів.
Модель запису: агрегати/інваріанти/транзакції; зберігання - реляційне/ключ-значення/подієвий лог.
Модель читання (проекції): матеріалізовані таблиці/індекси/кеш, синхронізуються асинхронно.
Узгодженість: часто eventual між записом і читанням; критичні шляхи - через пряме читання з write-моделі.


Архітектура (скелет)

1. Write-сервіс: приймає команди, валідує інваріанти, фіксує зміни (БД або події).
2. Outbox/CDC: гарантована публікація факту змін.
3. Процесори проекцій: слухають події/CDC і оновлюють read-моделі.
4. Read-сервіс: обслуговує queries з матеріалізованих уявлень/кешів/пошуку.
5. Саги/оркестрація: координують крос-агрегатні процеси.
6. Спостережуваність: лаг проекцій, відсоток успішних застосувань, DLQ.


Проектування моделі запису

Агрегати: чіткі межі транзакцій (наприклад, «Order», «Payment», «UserBalance»).
Інваріанти: формалізуйте (грошові суми ≥ 0, унікальність, ліміти).
Команди ідемпотентні за ключем (наприклад,'idempotency _ key').
Транзакції мінімальні за охопленням; зовнішні побічні ефекти - через outbox.

Приклад команди (псевдо-JSON)

json
{
"command": "CapturePayment",
"payment_id": "pay_123",
"amount": 1000,
"currency": "EUR",
"idempotency_key": "k-789",
"trace_id": "t-abc"
}

Проектування моделі читання

Відштовхуйтесь від запитів: які екрани/звіти потрібні?
Денормалізація допустима: read-модель - «оптимізований кеш».
Кілька проекцій для різних завдань: пошук (OpenSearch), звіти (колонночное сховище), картки (KV/Redis).
TTL і перезбірка: проекції повинні вміти відновлюватися з джерела (реплей подій/снапшоти).


Узгодженість і UX

Eventual consistency: інтерфейс може короткочасно показувати старі дані.
UX-патерни: «дані оновлюються»..., optimistic UI, індикатори синхронізації, блокування небезпечних дій до підтвердження.
Для операцій, що вимагають сильної узгодженості (наприклад, показ точного балансу перед списанням), читайте безпосередньо з write-моделі.


CQRS і Event Sourcing (за бажанням)

Event Sourcing (ES) зберігає події, а стан агрегату - результат їх згортки.
Зв'язка CQRS + ES дає ідеальний аудит і легку перезбірку проекцій, але підвищує складність.
Альтернатива: звичайна OLTP-БД + outbox/CDC → проекції.


Реплікація: Outbox и CDC

Outbox (в одній транзакції): запис доменних змін + запис події в outbox; паблішер доставляє в шину.
CDC: зчитування з логу БД (Debezium тощо) → трансформація в доменні події.
Гарантії: за замовчуванням at-least-once, споживачі та проекції повинні бути ідемпотентні.


Вибір сховищ

Write: реляційні (PostgreSQL/MySQL) для транзакцій; KV/Document - де інваріанти прості.

Read:
  • KV/Redis - картки та швидкі ключові читання;
  • Пошук (OpenSearch/Elasticsearch) - пошук/фільтри/фасети;
  • Колонкові (ClickHouse/BigQuery) - звіти;
  • Кеш на CDN - публічні каталоги/контент.

Патерни інтеграції

API шар: окремі ендпоінти/сервіси для'commands'і'queries'.
Ідемпотентність: ключ операції в заголовку/тілі; зберігання recent-keys з TTL.
Саги/оркестрація: тайм-аути, компенсації, повторюваність кроків.
Backpressure: обмеження паралелізму процесорів проекцій.


Спостережуваність

Метрики write: p95/99 латентності команд, частка успішних транзакцій, помилки валідації.
Метрики read: p95/99 запитів, hit-rate кешу, навантаження на пошуковий кластер.
Лаг проекцій (час і повідомлення), DLQ-ставка, відсоток дедуплікацій.
Трейсинг: 'trace _ id'проходить через команду → outbox → проекцію → query.


Безпека та комплаєнс

Поділ прав: різні scopes/ролі для запису і читання; принцип найменших привілеїв.
PII/PCI: мінімізуйте в проекціях; шифрування at-rest/in-flight; маскування.
Аудит: фіксуйте команду, актора, результат,'trace _ id'; WORM-архіви для критичних доменів (платежі, KYC).


Тестування

Contract tests: для команд (помилки, інваріанти) і queries (формати/фільтри).
Projection tests: подайте серію подій/CDC і перевірте підсумкову read-модель.
Chaos/latency: інжекція затримок в процесори проекцій; перевірка UX при лагу.
Replayability: перезбирання проекцій на стенді зі снапшотів/логу.


Міграції та еволюція

Нові поля - адитивно в події/CDC; read-моделі пересобираються.
Подвійний запис (dual-write) при редизайні схем; старі проекції тримаємо до перемикання.
Версіонування: 'v1 '/' v2'подій і ендпоінтів, Sunset-план.
Feature flags: введення нових queries/проекцій по канарці.


Антипатерни

CQRS «заради моди» в простих CRUD-сервісах.
Жорстка синхронна залежність читання від запису (вбиває ізоляцію і стійкість).
Один індекс на всі: змішування різнорідних запитів в один read-store.
Немає ідемпотентності у проекцій → дублі і розбіжності.
Невідновлювані проекції (немає реплея/снапшотів).


Приклади доменів

Платежі (онлайн-сервіс)

Write: 'Authorize','Capture','Refund'в транзакційній БД; outbox публікує'payment.'.

Read:
  • Redis «картка платежу» для UI;
  • ClickHouse для звітів;
  • OpenSearch для пошуку транзакцій.
  • Критичний шлях: авторизація ≤ 800 мс p95; узгодженість читання для UI - eventual (до 2-3 с).

KYC

Write: команди на старт/апдейт статусу; зберігання PII в захищеній БД.
Read: полегшена проекція статусів без PII; PII підтягується точково при необхідності.
Безпека: різні scopes на читання статусу і доступ до документів.

Баланси (iGaming/фінанси)

Write: агрегат «UserBalance» з атомарними інкрементами/декрементами; ідемпотентні ключі на операцію.
Read: кеш для «швидкого балансу»; для списання - пряме читання з write (сувора узгодженість).
Сага: депозити/висновки координуються подіями, при збоях - компенсації.


Чек-лист впровадження

  • Виділені агрегати та інваріанти write-моделі.
  • Визначені ключові queries і спроектовані проекції під них.
  • Налаштовані outbox/CDC і ідемпотентні процесори проекцій.
  • Є план перезбірки проекцій (snapshot/replay).
  • SLO: латентність команд, лаг проекцій, доступність read/write окремо.
  • Розділені права доступу та шифрування даних реалізовано.
  • Алерти на DLQ/лаг/провали дедуплікації.
  • Тести: контракти, проекції, хаос, реплей.

FAQ

Чи обов'язково Event Sourcing для CQRS?
Ні, ні. Можна будувати на звичайній БД + outbox/CDC.

Як боротися з розсинхронізацією?
Явно проектувати UX, вимірювати лаг проекцій, давати критичним операціям читати з write.

Чи можна в одному сервісі тримати і write, і read?
Так, фізичний поділ - опціонально; логічний поділ відповідальності обов'язковий.

Що щодо транзакцій між агрегатами?
Через саги і події; уникайте розподілених транзакцій, якщо можна.


Підсумок

CQRS розв'язує руки: тонкий, надійний шлях запису з чіткими інваріантами і швидкі, цільові читання з матеріалізованих проекцій. Це підвищує продуктивність, спрощує еволюцію і робить систему стійкішою до навантажень - якщо дисципліновано управляти узгодженістю, спостережуваністю і міграціями.

Contact

Зв’яжіться з нами

Звертайтеся з будь-яких питань або за підтримкою.Ми завжди готові допомогти!

Розпочати інтеграцію

Email — обов’язковий. Telegram або WhatsApp — за бажанням.

Ваше ім’я необов’язково
Email необов’язково
Тема необов’язково
Повідомлення необов’язково
Telegram необов’язково
@
Якщо ви вкажете Telegram — ми відповімо й там, додатково до Email.
WhatsApp необов’язково
Формат: +код країни та номер (наприклад, +380XXXXXXXXX).

Натискаючи кнопку, ви погоджуєтесь на обробку даних.