CQRS и Event Sourcing в Rust: Архитектура для бескомпромиссной производительности

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему CQRS и Event Sourcing — это не «оверкилл», а спасение для сложных систем

Вы когда-нибудь сталкивались с ситуацией, когда ваша система, написанная на Rust, идеально справляется с запросами на чтение, но «падает на колени» при записи? Или наоборот — вы жертвуете производительностью чтения ради возможности сохранить полную историю изменений? Знакомо? Тогда добро пожаловать в мир CQRS и Event Sourcing.

Многие разработчики считают эти паттерны «тяжелой артиллерией», применимой только в enterprise-проектах на C# или Java. Но с ростом популярности Rust в высоконагруженных системах (от игровых серверов до финансовых платформ) CQRS и Event Sourcing становятся не просто полезными — они становятся необходимыми. Rust с его нулевой стоимостью абстракций и строгой типизацией — идеальный язык для реализации этих паттернов.

В этой статье мы не просто поговорим о теории. Мы напишем рабочий код на Rust, который вы сможете адаптировать под свои проекты. Вы узнаете, как разделить ответственность между командами и запросами, как хранить события вместо текущего состояния и как всё это ускоряет вашу систему в разы.

Что такое CQRS и Event Sourcing? Простым языком для Rust-разработчика

Представьте, что вы управляете банковским счетом. В традиционной CRUD-модели у вас есть одна таблица «Счета», где хранится текущий баланс. Если нужно посмотреть историю — вы создаете еще одну таблицу «Транзакции». Но что, если вам нужно не просто знать баланс, а понимать, почему он изменился? Или восстанавливать состояние на любую дату?

CQRS: Разделяй и властвуй

CQRS (Command Query Responsibility Segregation) — это принцип, который разделяет операции на две группы:

  • Команды (Commands) — изменяют состояние. Они не возвращают данные, только подтверждение или ошибку.
  • Запросы (Queries) — только читают данные. Они не меняют состояние.

В Rust это разделение выглядит естественно: типы команд и запросов — это разные структуры, а их обработчики — разные функции или модули. Это не просто «хорошая практика», это способ избежать случайных мутаций при чтении.

// Команда — изменяет состояние
pub struct CreateOrder {
    pub user_id: Uuid,
    pub items: Vec<OrderItem>,
}

// Запрос — только читает
pub struct GetOrderHistory {
    pub user_id: Uuid,
    pub limit: usize,
}

Event Sourcing: Храните историю, а не моментальный снимок

Event Sourcing — это подход, при котором мы сохраняем не текущее состояние объекта, а последовательность событий, которые к нему привели. Хотите узнать баланс счета на 1 января 2023 года? Просто «проиграйте» все события с начала до этой даты.

Преимущества очевидны:

  • Аудит — каждое изменение зафиксировано.
  • Временные срезы — можно восстановить любое состояние.
  • Устойчивость к багам — если в коде ошибка, вы не потеряли данные, а просто неправильно их «проиграли».

Практический пример: Система управления заказами на Rust

Давайте построим мини-систему для интернет-магазина. Она будет обрабатывать команды (создание заказа, добавление товара) и запросы (получение корзины, история заказов). Вся логика будет построена на событиях.

Шаг 1: Определяем события и команды

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

use serde::{Deserialize, Serialize};
use uuid::Uuid;

// События — неизменяемые факты
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OrderEvent {
    Created { order_id: Uuid, user_id: Uuid, timestamp: i64 },
    ItemAdded { order_id: Uuid, item: String, quantity: u32 },
    ItemRemoved { order_id: Uuid, item: String },
    Submitted { order_id: Uuid, timestamp: i64 },
}

// Команды — намерения
#[derive(Debug)]
pub enum OrderCommand {
    CreateOrder { user_id: Uuid },
    AddItem { order_id: Uuid, item: String, quantity: u32 },
    RemoveItem { order_id: Uuid, item: String },
    SubmitOrder { order_id: Uuid },
}

Шаг 2: Event Store — простое хранилище событий

Event Store — это сердце системы. В реальном проекте вы бы использовали PostgreSQL, DynamoDB или специализированное решение. Мы для простоты напишем in-memory хранилище. Главное — понять принцип: мы никогда не обновляем записи, только добавляем новые.

use std::collections::HashMap;
use std::sync::Mutex;

pub struct EventStore {
    // Каждый заказ — это вектор событий
    events: Mutex<HashMap<Uuid, Vec<OrderEvent>>>,
}

impl EventStore {
    pub fn new() -> Self {
        Self { events: Mutex::new(HashMap::new()) }
    }

    // Добавляем событие к существующей последовательности
    pub fn append(&self, order_id: Uuid, event: OrderEvent) {
        let mut store = self.events.lock().unwrap();
        store.entry(order_id).or_insert_with(Vec::new).push(event);
    }

    // Получаем все события для агрегата
    pub fn get_events(&self, order_id: &Uuid) -> Vec<OrderEvent> {
        self.events.lock().unwrap()
            .get(order_id)
            .cloned()
            .unwrap_or_default()
    }
}

Шаг 3: Обработчик команд — бизнес-логика на событиях

Теперь самое интересное. Обработчик команды не просто изменяет состояние, а генерирует событие. Если событие валидно — оно сохраняется. Если нет — возвращается ошибка.

#[derive(Debug)]
pub enum CommandError {
    OrderNotFound,
    OrderAlreadySubmitted,
    ItemNotInOrder,
}

pub struct CommandHandler {
    store: EventStore,
}

impl CommandHandler {
    pub fn new(store: EventStore) -> Self {
        Self { store }
    }

    pub fn handle(&self, command: OrderCommand) -> Result<(), CommandError> {
        match command {
            OrderCommand::CreateOrder { user_id } => {
                let order_id = Uuid::new_v4();
                let event = OrderEvent::Created {
                    order_id,
                    user_id,
                    timestamp: chrono::Utc::now().timestamp(),
                };
                self.store.append(order_id, event);
                Ok(())
            }
            OrderCommand::AddItem { order_id, item, quantity } => {
                // Проверяем, существует ли заказ и не отправлен ли он
                let events = self.store.get_events(&order_id);
                if events.is_empty() {
                    return Err(CommandError::OrderNotFound);
                }
                // Проверяем, не был ли заказ уже отправлен
                if events.iter().any(|e| matches!(e, OrderEvent::Submitted { .. })) {
                    return Err(CommandError::OrderAlreadySubmitted);
                }
                let event = OrderEvent::ItemAdded {
                    order_id,
                    item,
                    quantity,
                };
                self.store.append(order_id, event);
                Ok(())
            }
            // ... аналогично для других команд
            _ => unimplemented!()                

Блоги

Рекомендаций по книгам