CQRS и Event Sourcing в Java: Архитектура, которая не боится сложности

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

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

Начать курс

Введение: Когда CRUD перестаёт справляться

Представьте, что вы пишете систему для биржевых торгов. Каждую секунду — тысячи ордеров. Каждый ордер — это не просто "вставить запись". Это проверка баланса, пересчёт портфеля, аудит, уведомления. А теперь представьте, что вам нужно не только записать текущее состояние, но и восстановить, что произошло в 14:35:22.003 вчера. Обычный CRUD (Create, Read, Update, Delete) тут ломается. Добро пожаловать в мир CQRS и Event Sourcing на Java.

Эти два паттерна — не просто модные слова. Это инструменты, которые позволяют строить системы с высокой производительностью, полным аудитом и возможностью "перемотки времени". В этой статье мы разберём, как реализовать CQRS (Command Query Responsibility Segregation) и Event Sourcing в Java, используя современные фреймворки и чистый код. Без воды, с примерами.

Что такое CQRS простыми словами

CQRS — это разделение ответственности между командами (запись) и запросами (чтение). В классическом CRUD вы используете одну и ту же модель для чтения и записи. В CQRS вы создаёте две разные модели: одну для изменения данных (команды), другую для их извлечения (запросы).

Почему это нужно?

  • Производительность: Чтение и запись масштабируются независимо. Можно иметь 10 реплик для чтения и 2 для записи.
  • Безопасность: Команды могут проверять бизнес-правила, а запросы — просто отдавать данные.
  • Гибкость: Для чтения можно использовать денормализованные представления, для записи — нормализованные.

Пример кода: Разделение команд и запросов

Допустим, у нас есть система управления заказами. Вместо того чтобы делать OrderService.save(order), мы разделяем:

// Команда — это намерение изменить состояние
public class CreateOrderCommand {
    private final String orderId;
    private final String customerId;
    private final List<OrderItem> items;

    // конструктор, геттеры...
}

// Обработчик команды
@Component
public class CreateOrderCommandHandler {
    @Autowired
    private OrderRepository orderRepository;

    public void handle(CreateOrderCommand command) {
        // Валидация бизнес-правил
        if (command.getItems().isEmpty()) {
            throw new IllegalArgumentException("Order must have at least one item");
        }
        Order order = new Order(command.getOrderId(), command.getCustomerId(), command.getItems());
        orderRepository.save(order);
    }
}

// Запрос — это получение данных
public class GetOrderQuery {
    private final String orderId;
    // конструктор, геттеры...
}

// Обработчик запроса
@Component
public class GetOrderQueryHandler {
    @Autowired
    private OrderReadRepository orderReadRepository;

    public OrderDTO handle(GetOrderQuery query) {
        // Прямое чтение из оптимизированной БД
        return orderReadRepository.findById(query.getOrderId());
    }
}

Обратите внимание: OrderRepository (запись) и OrderReadRepository (чтение) — это разные интерфейсы. Они могут указывать на разные базы данных или разные схемы.

Event Sourcing: Храним не состояние, а историю

Event Sourcing — это паттерн, при котором мы сохраняем не текущее состояние объекта, а последовательность событий, которые к этому состоянию привели. Вместо "Заказ №123: статус = DELIVERED" мы храним: "Событие 1: Заказ создан", "Событие 2: Товар добавлен", "Событие 3: Заказ оплачен", "Событие 4: Заказ доставлен".

Как это работает в Java

Каждое событие — это неизменяемый объект (immutable). Мы сохраняем их в Event Store (специализированная БД). Текущее состояние агрегата вычисляется путём воспроизведения (replaying) всех событий.

// Базовое событие
public interface DomainEvent {
    String getAggregateId();
    Instant getOccurredOn();
}

// Конкретное событие
public class OrderCreatedEvent implements DomainEvent {
    private final String orderId;
    private final String customerId;
    private final Instant occurredOn;

    // конструктор, геттеры...
}

// Агрегат — корень транзакционной согласованности
public class OrderAggregate {
    private String orderId;
    private String status;
    private List<DomainEvent> uncommittedEvents = new ArrayList<>();

    public OrderAggregate(String orderId, String customerId) {
        apply(new OrderCreatedEvent(orderId, customerId, Instant.now()));
    }

    public void apply(OrderCreatedEvent event) {
        this.orderId = event.getOrderId();
        this.status = "CREATED";
        uncommittedEvents.add(event);
    }

    public List<DomainEvent> getUncommittedEvents() {
        return uncommittedEvents;
    }

    // Восстановление из истории
    public OrderAggregate(List<DomainEvent> events) {
        for (DomainEvent event : events) {
            when(event);
        }
    }

    private void when(DomainEvent event) {
        if (event instanceof OrderCreatedEvent) {
            apply((OrderCreatedEvent) event);
        }
        // другие обработчики...
    }
}

Когда вы вызываете new OrderAggregate("123", "cust1"), создаётся событие OrderCreatedEvent. Оно сохраняется в Event Store. Чтобы получить текущее состояние, вы загружаете все события для этого агрегата и воспроизводите их.

Объединяем CQRS и Event Sourcing

Вместе эти паттерны работают как часы: CQRS разделяет каналы записи и чтения, а Event Sourcing предоставляет источник истины через события. Команды генерируют события, события сохраняются, а проекции (read models) подписываются на события и обновляют свои данные для чтения.

Архитектура в деталях

  1. Command Bus — принимает команды и направляет их обработчикам.
  2. Aggregate — проверяет бизнес-правила и генерирует события.
  3. Event Store — сохраняет события (например, PostgreSQL, MongoDB, EventStoreDB).
  4. Event Bus — публикует события для подписчиков.
  5. Projectors — слушают события и обновляют read models.
  6. Read Models — денормализованные таблицы для быстрых запросов.

Пример полного цикла с Axon Framework

Axon Framework — это популярный Java-фреймворк, который реализует CQRS и Event Sourcing "из коробки". Вот как выглядит типичный сценарий:

// 1. Команда
public class PlaceOrderCommand {
    @TargetAggregateIdentifier
    private final String orderId;
    private final String productId;
    private final int quantity;
    // конструктор...
}

// 2. Агрегат с Axon
@Aggregate
public class OrderAggregate {
    @AggregateIdentifier
    private String orderId;
    private boolean placed;

    public OrderAggregate() {
        // Required by Axon
    }

    @CommandHandler
    public OrderAggregate(PlaceOrderCommand cmd) {
        // бизнес-логика
        if (cmd.getQuantity() <= 0) {
            throw new IllegalArgumentException("Quantity must be positive");
        }
        apply(new                

Блоги

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