Event Sourcing — це підхід до зберігання даних, де головним джерелом істини (source of truth) є події. Замість того, щоб зберігати поточний стан об'єкта, ми зберігаємо всі зміни, які коли-небудь з ним відбувалися.
Як це працює? Уяви банківський рахунок. Замість того, щоб зберігати, скажімо, поточний баланс, ми зберігаємо всі операції: поповнення, зняття, перекази. Поточний стан балансу — це результат "програвання" усіх цих операцій. Це як дивитися історію браузера: щоб зрозуміти, як ти опинився на поточній сторінці, ти просто переглядаєш всі попередні переходи.
Події:
1. Поповнення: +1000 умовні одиниці
2. Зняття: -200 умовні одиниці
3. Поповнення: +500 умовні одиниці
Поточний стан:
Баланс: 1300 умовні одиниці (обчислюється з подій)
Це дозволяє не тільки знати "де ти зараз", але й зрозуміти "як ти сюди потрапив".
Зв'язок між CQRS і Event Sourcing
Щоб зрозуміти, як ці два підходи пов'язані, давай розберемо їх по поличках.
CQRS: розділення світу на два
CQRS ділить операції системи на дві сфери: командний шар (Command) і шар запитів (Query). Команди змінюють стан системи, а запити лише читають дані. І от тут з'являється цікава ідея: а що як для зміни даних (Command) використовувати події?
Event Sourcing як потужна основа CQRS
Event Sourcing ідеально лягає на Command-частину CQRS. Замість того, щоб миттєво зберігати зміни в базу даних, ви зберігаєте події, які описують зміни. А вже потім, на основі цих подій, оновлюєте стан або створюєте нові моделі для читання даних (Query).
Дивись, як це виглядає на практиці:
- Користувач відправляє команду
зняти гроші(наприклад, 500 умовних одиниць з рахунку). - Замість того, щоб одразу оновити баланс в базі даних, система створює подію:
MoneyWithdrawn {amount: 500}. - Ця подія зберігається в сховищі подій.
- Сховище подій пізніше обробляє цю подію, оновлюючи моделі для читання (наприклад, "баланс користувача").
Отже, основний принцип тут: "Спочатку подія, потім наслідки".
Переваги їх інтеграції
Коли ти об'єднуєш CQRS і Event Sourcing, ти отримуєш кілька класних бонусів:
- Повна історія змін. Ти можеш повернутися назад у часі і зрозуміти, як об'єкт прийшов до поточного стану. Це дуже зручно для аудиту або відладки.
- Можливість повторного відтворення. Якщо потрібно перерахувати поточний стан в іншій базі даних або відновити систему після збою — ти просто програєш усі події.
- Автоматичне відділення команд і запитів. Event Sourcing органічно вписується в підхід CQRS. Події — це результат команд, а підготовлені моделі даних для запитів (Query) — це твій читабельний результат.
- Асинхронність. Подію можна обробити пізніше. Це знижує навантаження на Command-шар і дозволяє системі масштабуватися.
Реалізація на практиці
Давай спробуємо реалізувати базовий приклад з використанням CQRS разом з Event Sourcing. Ми продовжимо розробку нашого додатку для управління замовленнями в мікросервісній архітектурі.
Припустимо, наше додаток обробляє замовлення. Нам необхідно:
- Створити замовлення.
- Оновити статус замовлення.
- Підготувати модель для користувача, щоб він міг бачити поточний статус усіх своїх замовлень.
Крок 1: Створення подій
Створимо Java-класи, які представляють наші події.
// Подія створення замовлення
public class OrderCreatedEvent {
private final String orderId;
private final String customerId;
private final String product;
public OrderCreatedEvent(String orderId, String customerId, String product) {
this.orderId = orderId;
this.customerId = customerId;
this.product = product;
}
// Гетери
}
// Подія оновлення статусу замовлення
public class OrderStatusUpdatedEvent {
private final String orderId;
private final String status;
public OrderStatusUpdatedEvent(String orderId, String status) {
this.orderId = orderId;
this.status = status;
}
// Гетери
}
Крок 2: Сховище подій
Нам потрібно місце, де ми будемо зберігати події. Нехай для початку це буде простий список.
import java.util.ArrayList;
import java.util.List;
public class EventStore {
private final List<Object> events = new ArrayList<>();
public void saveEvent(Object event) {
events.add(event);
}
public List<Object> getEvents() {
return new ArrayList<>(events);
}
}
Крок 3: Command Handler
Обробник команд відповідає за створення подій на основі вхідних команд.
import java.util.UUID;
public class OrderCommandHandler {
private final EventStore eventStore;
public OrderCommandHandler(EventStore eventStore) {
this.eventStore = eventStore;
}
public void handleCreateOrder(String customerId, String product) {
String orderId = UUID.randomUUID().toString();
OrderCreatedEvent event = new OrderCreatedEvent(orderId, customerId, product);
eventStore.saveEvent(event);
}
public void handleUpdateOrderStatus(String orderId, String status) {
OrderStatusUpdatedEvent event = new OrderStatusUpdatedEvent(orderId, status);
eventStore.saveEvent(event);
}
}
Крок 4: Query Layer
Шар запитів обробляє події і готує агреговані дані для читання.
import java.util.HashMap;
import java.util.Map;
public class OrderQueryService {
private final Map<String, String> orderStatus = new HashMap<>();
public void applyEvent(Object event) {
if (event instanceof OrderCreatedEvent created) {
orderStatus.put(created.getOrderId(), "CREATED");
} else if (event instanceof OrderStatusUpdatedEvent updated) {
orderStatus.put(updated.getOrderId(), updated.getStatus());
}
}
public String getOrderStatus(String orderId) {
return orderStatus.get(orderId);
}
}
Крок 5: Інтеграція
Тепер об'єднаємо все разом.
public class Main {
public static void main(String[] args) {
EventStore eventStore = new EventStore();
OrderCommandHandler commandHandler = new OrderCommandHandler(eventStore);
OrderQueryService queryService = new OrderQueryService();
// Створюємо замовлення
commandHandler.handleCreateOrder("customer123", "Product A");
// Оновлюємо статус замовлення
String orderId = eventStore.getEvents()
.stream()
.filter(event -> event instanceof OrderCreatedEvent)
.map(event -> ((OrderCreatedEvent) event).getOrderId())
.findFirst()
.orElseThrow();
commandHandler.handleUpdateOrderStatus(orderId, "SHIPPED");
// Обробляємо події в Query Service
eventStore.getEvents().forEach(queryService::applyEvent);
// Отримуємо статус замовлення
System.out.println("Статус замовлення: " + queryService.getOrderStatus(orderId));
}
}
Основні виклики і типові помилки
Проблема з продуктивністю
При великій кількості подій програвання всього event log може стати вузьким місцем. Рішення? Використовуй snapshots — збережені проміжні стани.
Складність у налагодженні
Подійно-орієнтовані системи складніше налагоджувати через асинхронний характер. Хороші логи і розподілена трасування (наприклад, Spring Sleuth) врятують життя.
Консистентність даних
Важливо, щоб команди і події оброблялися в правильній послідовності. Інакше буде хаос.
CQRS і Event Sourcing — потужний дует для масштабованих і відмовостійких мікросервісів. Вони дозволяють розділити логіку запису і читання, зберігати повну історію дій і працювати з даними асинхронно. Якщо їх правильно налаштувати, вони стануть твоїми вірними союзниками, а не головним болем.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ