Паттерн CQRS

Command-Query Separation - принцип разделения команд и запросов, предложил Бертран Мейер. разделяй и властвуй - принцип который применил Юлий Цезарь при завоевании Галлии, А теперь и мы изучим.

Контекст
ПРоблемная Маша:
Как ты помнишь у нас микры и паттерн Database per service. Не просто достать данные, когда надо еще несколько микров дернуть. А теперь еще Event sourcing применили и совсем усложнилась логика запросов...
Выручающий Саша:
Так сделайте два микра, в одном get, а в другом post, put, delete
ПРоблемная Маша:
И что нам это даст?
Выручающий Саша:
Логика не будет такой громоздкой.
А самое главное сможете масштабировать реплики независимо. Если запросов много а команд мало, то можно только микр запросов масштабировать.
Проблема
В сложных приложениях реализация привычных CRUD может стать громоздкой, так как при чтении может идти еще куча дополнительных запросов к другим микрам, а при записи могут быть сложные проверки, в итоге громоздкая модель. Плюс лишние затраты при масштабировании если рабочие нагрузки чтения и записи сильно отличаются или есть разные требования к производительности для чтения и для записи.
Решение
CQRS разделяет операции чтения и записи на разные модели, используя команды для обновления данных и запросы для чтения данных.
Схемы реализации
Пример кода
Создадим Order Service, который будет обладать следующим функционалом:

🔹 Команды изменяют состояние и генерируют события
🔹 События хранятся в специальном хранилище
🔹 Проекции (Read Model) формируются из событий для быстрого чтения
События (Events)
// Базовый интерфейс событий
public interface Event {}

// Событие создания заказа
public class OrderCreatedEvent implements Event {
    private final String orderId;
    private final String product;

    public OrderCreatedEvent(String orderId, String product) {
        this.orderId = orderId;
        this.product = product;
    }

    public String getOrderId() { return orderId; }
    public String getProduct() { return product; }
}
Команды (Commands)
public class CreateOrderCommand {
    private final String orderId;
    private final String product;

    public CreateOrderCommand(String orderId, String product) {
        this.orderId = orderId;
        this.product = product;
    }

    public String getOrderId() { return orderId; }
    public String getProduct() { return product; }
}
Хранилище событий
public class EventStore {
    private final List<Event> events = new ArrayList<>();

    public void save(Event event) {
        events.add(event);
    }

    public List<Event> getEvents() {
        return new ArrayList<>(events);
    }
}
Управление командами
public class OrderCommandHandler {
    private final EventStore eventStore;

    public OrderCommandHandler(EventStore eventStore) {
        this.eventStore = eventStore;
    }

    public void handle(CreateOrderCommand command) {
        // Логика проверки, бизнес-валидации и т.д.
        OrderCreatedEvent event = new OrderCreatedEvent(command.getOrderId(), command.getProduct());
        eventStore.save(event);
        System.out.println("OrderCreatedEvent сохранено: " + command.getOrderId());
    }
}
Проекция для чтения (Read Model)
public class OrderReadModel {
    private final Map<String, String> orders = new HashMap<>();

    public void apply(Event event) {
        if (event instanceof OrderCreatedEvent e) {
            orders.put(e.getOrderId(), e.getProduct());
        }
    }

    public String getProduct(String orderId) {
        return orders.get(orderId);
    }
}
Обработчик событий
public class EventProcessor {
    private final OrderReadModel readModel;

    public EventProcessor(OrderReadModel readModel) {
        this.readModel = readModel;
    }

    public void process(List<Event> events) {
        for (Event event : events) {
            readModel.apply(event);
        }
    }
}
🔜Использование
EventStore eventStore = new EventStore();
OrderReadModel readModel = new OrderReadModel();
OrderCommandHandler commandHandler = new OrderCommandHandler(eventStore);
EventProcessor processor = new EventProcessor(readModel);

// Создаём заказы
commandHandler.handle(new CreateOrderCommand("1", "Laptop"));
commandHandler.handle(new CreateOrderCommand("2", "Smartphone"));

// Обновляем проекцию для чтения
processor.process(eventStore.getEvents());

// Чтение данных
System.out.println(readModel.getProduct("1")); // Laptop
System.out.println(readModel.getProduct("2")); // Smartphone
Онлайн-школа профессионального программирования на java для коммерческих разработчиков и соискателей.
2 главные задачи, которые мы решаем:
  1. Трудоустройство и успешное прохождение испыталки.
  2. Переход на современный стек middle+
Наше главное достояние:
Менторская поддержка 24/7 и обучение в формате живого общения
Получить консультацию по обучению