Event Sourcing is an approach to storing data where the main source of truth (source of truth) is events. Instead of saving the current state of an object, we store all the changes that ever happened to it.
How does it work? Imagine a bank account. Instead of storing, say, the current balance, we store all operations: deposits, withdrawals, transfers. The current balance is the result of "replaying" all those operations. It's like looking at your browser history: to understand how you ended up on the current page, you just walk through all previous navigations.
Events:
1. Deposit: +1000 units
2. Withdrawal: -200 units
3. Deposit: +500 units
Current state:
Balance: 1300 units (calculated from events)
This lets you not only know "where you are now", but also understand "how you got here".
How CQRS and Event Sourcing Fit Together
To understand how these two approaches relate, let's break them down.
CQRS: split the world in two
CQRS splits system operations into two domains: the command side (Command) and the query side (Query). Commands change system state, while queries only read data. And here's an interesting idea: what if you used events to represent changes (Command)?
Event Sourcing as a solid foundation for CQRS
Event Sourcing fits perfectly on the Command side of CQRS. Instead of immediately persisting state changes to a database, you persist events that describe those changes. Then, based on those events, you update state or build read models for queries (Query).
Here's how this looks in practice:
- The user sends a command
withdraw money(for example, 500 units from the account). - Instead of immediately updating the balance in the database, the system creates an event:
MoneyWithdrawn {amount: 500}. - This event is stored in the event store.
- The event store later processes this event, updating read models (for example, the "user balance").
So, the core principle here is: "First the event, then the effects".
Benefits of Integrating Them
When you combine CQRS and Event Sourcing, you get some nice bonuses:
- Full history of changes. You can go back in time and understand how an object reached its current state. That's very handy for auditing or debugging.
- Ability to replay. If you need to recalculate current state in another database or recover the system after a failure — you just replay all the events.
- Automatic separation of commands and queries. Event Sourcing naturally fits into the CQRS approach. Events are the Commands, and the prepared read models (Query) are your readable result.
- Asynchrony. Events can be processed later. This reduces load on the Command side and helps the system scale.
Practical Implementation
Let's try to implement a basic example using CQRS together with Event Sourcing. We'll continue developing our order management app in a microservice architecture.
Assume our app handles orders. We need to:
- Create an order.
- Update order status.
- Prepare a model for the user so they can see the current status of all their orders.
Step 1: Define events
Let's create Java classes that represent our events.
// Order creation event
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;
}
// Getters
}
// Order status update event
public class OrderStatusUpdatedEvent {
private final String orderId;
private final String status;
public OrderStatusUpdatedEvent(String orderId, String status) {
this.orderId = orderId;
this.status = status;
}
// Getters
}
Step 2: Event store
We need a place to save events. For now let's keep it as a simple list.
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);
}
}
Step 3: Command Handler
The command handler is responsible for creating events based on incoming commands.
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);
}
}
Step 4: Query Layer
The query side processes events and prepares aggregated data for reading.
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);
}
}
Step 5: Wiring it up
Now let's wire everything together.
public class Main {
public static void main(String[] args) {
EventStore eventStore = new EventStore();
OrderCommandHandler commandHandler = new OrderCommandHandler(eventStore);
OrderQueryService queryService = new OrderQueryService();
// Create an order
commandHandler.handleCreateOrder("customer123", "Product A");
// Update order status
String orderId = eventStore.getEvents()
.stream()
.filter(event -> event instanceof OrderCreatedEvent)
.map(event -> ((OrderCreatedEvent) event).getOrderId())
.findFirst()
.orElseThrow();
commandHandler.handleUpdateOrderStatus(orderId, "SHIPPED");
// Process events in the Query Service
eventStore.getEvents().forEach(queryService::applyEvent);
// Get order status
System.out.println("Order status: " + queryService.getOrderStatus(orderId));
}
}
Main Challenges and Common Mistakes
Performance issues
With a large number of events, replaying the entire event log can become a bottleneck. Solution? Use snapshots — saved intermediate states.
Difficulty debugging
Event-driven systems are harder to debug because of their asynchronous nature. Good logs and distributed tracing (for example, Spring Sleuth) will save your life.
Data consistency
It's important that commands and events are processed in the correct order. Otherwise, you'll have chaos.
CQRS and Event Sourcing are a powerful combo for scalable, resilient microservices. They let you separate write and read logic, keep a full history of actions, and work with data asynchronously. If set up correctly, they'll be your allies rather than a headache.
GO TO FULL VERSION