CQRS and Event Sourcing: A Practical Introduction
CQRS and Event Sourcing: A Practical Introduction
You’ve probably heard the terms CQRS and Event Sourcing thrown around in architecture discussions. Maybe you’ve seen them associated with microservices, DDD, or high-performance systems. But what do they actually mean in practice? And more importantly—should you use them?
In this post, I’ll walk through the core concepts of CQRS (Command Query Responsibility Segregation) and Event Sourcing with concrete Java examples. We’ll look at real trade-offs, common pitfalls, and when these patterns actually shine.
The Problem with Traditional CRUD
Before diving into patterns, let’s look at a typical CRUD-based system. Imagine a simple banking application:
1 |
|
This works fine for simple cases. But as your system grows, you notice problems:
- Read and write models are coupled. The same entity handles both commands (deposit/withdraw) and queries (get balance). If you want different representations for reads (e.g., a dashboard showing transaction history), you either add more fields or create separate DTOs that still depend on the same entity.
- Performance suffers. Complex queries (aggregations, reporting) slow down writes because they share the same data store.
- Audit trails are hard. To track changes, you need additional tables or columns (e.g.,
updated_at,changed_by). Even then, reconstructing historical state is painful. - Conflict resolution is tricky. In concurrent systems, locking strategies become complex and error-prone.
These problems are not hypothetical—I’ve seen them in production systems handling millions of transactions. CQRS and Event Sourcing address these issues head-on.
CQRS: Separating Commands from Queries
CQRS is a pattern that separates read operations (queries) from write operations (commands). Instead of a single model, you have two distinct models:
- Command Model: Handles writes. Validates business rules, enforces invariants, and produces events. Returns void or a simple acknowledgment.
- Query Model: Handles reads. Optimized for specific query needs, can be denormalized, and may use different storage technology.
A Simple CQRS Example in Java
Let’s refactor the banking example. First, the command side:
1 | // Command |
Now the query side—completely separate:
1 | // Query |
Notice that the query handler uses a different repository (AccountReadModelRepository). This repository might back a denormalized table, a Redis cache, or even a search index—whatever suits the read use case.
Benefits of CQRS
- Independent scaling: Reads and writes can scale separately. If you have a read-heavy workload, you can add more read replicas without affecting writes.
- Optimized models: Write models enforce consistency; read models are denormalized for fast queries.
- Security: You can apply different authorization rules to commands and queries.
- Team autonomy: Different teams can own the read and write sides, each using their preferred technology.
But CQRS also adds complexity. You now have two models to maintain, and eventual consistency between them must be handled. Which brings us to Event Sourcing.
Event Sourcing: Store Events, Not State
Event Sourcing is a pattern where you store all changes to an application state as a sequence of events. Instead of persisting the current state, you persist the events that led to that state.
How It Works
In the banking example, instead of storing the current balance, you store events like:
1 | public interface DomainEvent {} |
To get the current state, you replay all events for an aggregate:
1 | public class Account { |
Notice that deposit() returns events instead of modifying state directly. The caller is responsible for persisting those events.
Event Store
An event store is a database optimized for storing events. It typically:
- Appends events sequentially
- Loads all events for an aggregate by its ID
- Supports optimistic concurrency (e.g., using version numbers)
Here’s a simple in-memory event store:
1 | public class InMemoryEventStore { |
Combining CQRS and Event Sourcing
CQRS and Event Sourcing complement each other naturally:
- Commands produce events.
- Events are stored in the event store.
- Projections consume events and update the read model.
Here’s how the command handler looks with Event Sourcing:
1 | public class DepositCommandHandler { |
And a simple projection that updates the read model:
1 | public class AccountBalanceProjection { |
When to Use CQRS + Event Sourcing
These patterns are powerful but not a silver bullet. Use them when:
- Audit and compliance are critical. Event Sourcing gives you a complete, immutable history.
- Complex business logic with many state changes. Think order management, banking, or inventory systems.
- You need temporal queries. “What did the account look like last Tuesday?” becomes trivial.
- High write throughput with eventual consistency. CQRS allows you to batch updates to read models.
Avoid them when:
- Simple CRUD is sufficient. Don’t over-engineer.
- Strong consistency is required immediately. Eventual consistency can be problematic for some use cases (e.g., real-time bidding).
- Your team is new to the patterns. The learning curve is steep.
Real-World Trade-offs
Let me share some hard-earned lessons:
Event Versioning
Events change over time. A MoneyDeposited event might later need a transactionId field. You must handle versioning:
1 | // Version 1 |
Eventual Consistency
Read models are updated asynchronously. If a user deposits money and immediately queries the balance, they might see the old value. Solutions:
- Use a synchronous projection for critical reads.
- Show a “processing” indicator.
- Use optimistic UI updates.
Storage Costs
Event stores grow indefinitely. You’ll need strategies like snapshotting (periodically saving the current state to avoid replaying all events).
1 | public class AccountSnapshot { |
A Practical Example: Order Management
Let’s tie everything together with a more realistic example: an order management system.
Commands
1 | public class PlaceOrderCommand { |
Events
1 | public class OrderPlaced implements DomainEvent { |
Projections
A projection for the customer dashboard:
1 | public class CustomerOrderProjection { |
Conclusion
CQRS and Event Sourcing are powerful patterns that solve real problems in complex systems. They give you auditability, scalability, and flexibility. But they also introduce significant complexity—event versioning, eventual consistency, and storage management are real challenges.
My advice: start small. Maybe just implement CQRS without Event Sourcing first. Or use Event Sourcing for a single bounded context. Learn the patterns incrementally.
In production, I’ve seen these patterns transform systems—but only when applied thoughtfully. Don’t cargo-cult them. Understand the trade-offs, and you’ll know exactly when they’re the right tool.
Key Takeaways
- CQRS separates read and write models, allowing independent optimization and scaling.
- Event Sourcing stores state changes as an immutable sequence of events, enabling full auditability and temporal queries.
- Together, they form a powerful combination for systems with complex business logic and audit requirements.
- Event versioning and eventual consistency are the main challenges you’ll face.
- Start with a single bounded context—don’t apply these patterns globally from day one.
- Use snapshots to control event store growth and improve replay performance.