Event Sourcing and CQRS: From Theory to Production
Event Sourcing CQRS implementation is one of the most discussed yet misunderstood architectural patterns. Event Sourcing stores state changes as an immutable sequence of events, while CQRS (Command Query Responsibility Segregation) separates read and write models. Therefore, you get a complete audit trail, temporal queries, and optimized read/write paths — but at the cost of increased complexity.
Many teams adopt Event Sourcing for the wrong reasons or apply it too broadly. Moreover, the pattern introduces challenges around eventual consistency, event schema evolution, and debugging that aren’t apparent from conference talks. Consequently, this guide provides a realistic, production-focused perspective on when and how to implement these patterns effectively.
Event Store Implementation
The event store is the heart of an event-sourced system — an append-only log of all domain events. Each aggregate has its own event stream identified by a unique ID. Furthermore, optimistic concurrency control ensures that concurrent writes to the same aggregate are detected and handled correctly.
// Event Store interface
public interface EventStore {
void appendToStream(String streamId, long expectedVersion, List events);
List readStream(String streamId);
List readStream(String streamId, long fromVersion);
}
// Domain events as records
public sealed interface OrderEvent permits OrderCreated, ItemAdded, OrderConfirmed, OrderShipped {
String orderId();
Instant timestamp();
}
public record OrderCreated(String orderId, String customerId, Instant timestamp) implements OrderEvent {}
public record ItemAdded(String orderId, String productId, int quantity, BigDecimal price, Instant timestamp) implements OrderEvent {}
public record OrderConfirmed(String orderId, BigDecimal total, Instant timestamp) implements OrderEvent {}
public record OrderShipped(String orderId, String trackingNumber, Instant timestamp) implements OrderEvent {}
// Aggregate rebuilt from events
public class Order {
private String id;
private String customerId;
private List items = new ArrayList<>();
private OrderStatus status;
private BigDecimal total;
// Rebuild state from event history
public static Order fromEvents(List events) {
Order order = new Order();
events.forEach(order::apply);
return order;
}
private void apply(OrderEvent event) {
switch (event) {
case OrderCreated e -> { id = e.orderId(); customerId = e.customerId(); status = DRAFT; }
case ItemAdded e -> { items.add(new OrderItem(e.productId(), e.quantity(), e.price())); }
case OrderConfirmed e -> { status = CONFIRMED; total = e.total(); }
case OrderShipped e -> { status = SHIPPED; }
}
}
} Event Sourcing CQRS Implementation: Read Projections
CQRS separates the write model (events) from read models (projections). Projections subscribe to events and build optimized views for specific queries. Furthermore, you can have multiple projections from the same event stream — one for the dashboard, one for search, one for reporting — each optimized for its specific access pattern.
// Read projection — optimized for order listing queries
@Component
public class OrderListProjection {
private final JdbcTemplate jdbc;
@EventHandler
public void on(OrderCreated event) {
jdbc.update("""
INSERT INTO order_list_view (order_id, customer_id, status, created_at)
VALUES (?, ?, 'DRAFT', ?)
""", event.orderId(), event.customerId(), event.timestamp());
}
@EventHandler
public void on(ItemAdded event) {
jdbc.update("""
UPDATE order_list_view
SET item_count = item_count + 1,
updated_at = ?
WHERE order_id = ?
""", event.timestamp(), event.orderId());
}
@EventHandler
public void on(OrderShipped event) {
jdbc.update("""
UPDATE order_list_view
SET status = 'SHIPPED', tracking_number = ?, updated_at = ?
WHERE order_id = ?
""", event.trackingNumber(), event.timestamp(), event.orderId());
}
}
// Query service — reads from projection, not events
@Service
public class OrderQueryService {
public List getRecentOrders(String customerId, int limit) {
return jdbc.query("""
SELECT * FROM order_list_view
WHERE customer_id = ? ORDER BY created_at DESC LIMIT ?
""", orderSummaryMapper, customerId, limit);
}
} Snapshots for Performance
As event streams grow, rebuilding aggregates from thousands of events becomes slow. Snapshots periodically save the current state, allowing reconstruction from the snapshot plus only recent events. Additionally, snapshots can be created asynchronously without blocking writes.
When NOT to Use Event Sourcing
Event Sourcing is overkill for simple CRUD applications, content management systems, and user preference storage. Moreover, it adds significant complexity to debugging (you must replay events to understand state), testing (projections are eventually consistent), and schema evolution (old events must remain compatible). Therefore, apply Event Sourcing selectively to domains where the audit trail and temporal queries provide genuine business value. See EventStore’s guide for more implementation details.
Key Takeaways
- Start with a solid foundation and build incrementally based on your requirements
- Test thoroughly in staging before deploying to production environments
- Monitor performance metrics and iterate based on real-world data
- Follow security best practices and keep dependencies up to date
- Document architectural decisions for future team members
In conclusion, Event Sourcing CQRS implementation is a powerful pattern when applied to the right problems. Use it for financial systems, order management, and any domain where knowing “what happened and when” is a business requirement. But be honest about the trade-offs — eventual consistency, increased complexity, and the need for robust event schema evolution strategies.