Event Sourcing and CQRS: Practical Implementation Beyond the Theory

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 architecture diagram
Event sourcing stores every state change as an immutable event, providing a complete audit trail

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.

Event sourcing performance optimization
Snapshots prevent performance degradation as event streams grow over time

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
Architecture decision making
Apply Event Sourcing where audit trails and temporal queries provide real business value

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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top