Skip to main content
event sourcing and cqrs in practice

When Event Sourcing Is Wrong: Complexity Tax, Simpler Alternatives, and the Decision Framework

10 min read Chapter 16 of 17

When Event Sourcing Is Wrong

Decision framework

The decision tree starts with three questions: Is event history a business asset? Do you need multiple fundamentally different read models? Does the team have distributed systems experience? Each “no” points to a simpler alternative. The complexity spectrum runs from plain CRUD through CRUD-with-audit and CQRS-only to the full ES+CQRS stack at the far end.

This book has spent fifteen chapters building an event-sourced system. It has shown the power of event streams, the flexibility of CQRS, the debugging advantage of time-travel, the operational complexity of projections, the coordination overhead of sagas, and the infrastructure demands of event stores, message brokers, and multiple read model databases. It would be intellectually dishonest to end without stating plainly: most systems should not use event sourcing.

Event sourcing is a tool for specific problems. Like any powerful tool, it is expensive to operate and dangerous when misapplied. This chapter examines the costs, the alternatives, and the decision framework.

The Complexity Tax

Every architectural pattern has a cost. Event sourcing’s cost is higher than most because it is a pervasive choice: once you store events as the source of truth, every part of the system adapts to that decision.

Developer productivity. A new developer joining an event-sourced codebase must understand aggregates, event streams, projections, eventual consistency, sagas, upcasting, and the distinction between commands, events, and queries. In a CRUD system, they need to understand a database table and a REST endpoint. The onboarding time for event sourcing is measured in weeks, not days.

Infrastructure overhead. A CRUD system needs an application server and a database. An event-sourced system with CQRS needs an application server, an event store, a message broker (Kafka), one or more read model databases (PostgreSQL, Redis, Elasticsearch), a projection engine, an outbox relay, and monitoring for all of them. Each component has its own failure modes, configuration, backup strategy, and upgrade path.

Cognitive load. In a CRUD system, the current state is in the database. In an event-sourced system, the current state is computed by replaying events. The developer must reason about event sequences, not database rows. Debugging requires understanding which events produced which state transitions. Every query goes through a projection that may be lagging. The mental model is fundamentally different.

Testing surface. Unit tests for aggregates, integration tests for the event store, tests for projections, tests for sagas, tests for upcasters, tests for the outbox relay, tests for Kafka consumers. The test matrix grows with the number of moving parts.

Operational burden. Projection lag monitoring. Saga timeout handling. Event store growth planning. Schema evolution with upcasting chains. Partition management. Archive strategies. Each of these is a production concern that does not exist in a CRUD system.

What You Actually Need

Teams adopt event sourcing for specific reasons. Many of those reasons have simpler solutions.

”We need an audit trail”

An append-only audit log table solves this without event sourcing. Every mutation writes a row to the audit table with the timestamp, user, action, and before/after state.

-- Simpler alternative
CREATE TABLE audit_log (
    id          BIGSERIAL PRIMARY KEY,
    entity_type VARCHAR(100) NOT NULL,
    entity_id   VARCHAR(255) NOT NULL,
    action      VARCHAR(50) NOT NULL,
    user_id     VARCHAR(255),
    before_state JSONB,
    after_state  JSONB,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

This gives you a complete audit trail without changing the application’s data model, query patterns, or consistency model. The current state is still in the primary table. The audit log is a secondary record.

When to use event sourcing instead: when the audit trail IS the data model. When the business requires not just “what changed” but “why it changed” and “what would the state be if this change had not happened.” Financial systems, regulatory compliance systems, and legal document systems often have this requirement.

”We need temporal queries”

Temporal databases (PostgreSQL with tstzrange columns, or dedicated temporal databases) support point-in-time queries without event sourcing. You can query “what was the state of this record on January 15th?” with SQL.

-- Simpler alternative: temporal table
CREATE TABLE orders_history (
    order_id    VARCHAR(255) NOT NULL,
    status      VARCHAR(50) NOT NULL,
    total       DECIMAL(10,2) NOT NULL,
    valid_from  TIMESTAMPTZ NOT NULL,
    valid_to    TIMESTAMPTZ,
    PRIMARY KEY (order_id, valid_from)
);

-- Query state at a point in time
SELECT * FROM orders_history
WHERE order_id = 'order-123'
AND valid_from <= '2026-01-15T10:00:00Z'
AND (valid_to IS NULL OR valid_to > '2026-01-15T10:00:00Z');

When to use event sourcing instead: when the temporal queries need to combine multiple entity states into a time-consistent view, or when the event stream itself (not the derived state) is what the business cares about.

”We need to scale reads independently from writes”

CQRS without event sourcing solves this. Separate the read model from the write model. Use database read replicas, materialized views, or a dedicated query database. The write side stores normalized data in a relational database. The read side projects that data into optimized query structures.

This is the position stated in Chapter 1: CQRS is a deployment pattern that does not require event sourcing. A PostgreSQL primary with read replicas, or a Change Data Capture pipeline from the primary to Elasticsearch, gives you independent read scaling without the complexity of event streams, projections, and eventual consistency management.

When to use event sourcing instead: when the read models need to be computed from business events rather than database state changes. When the same events need to produce fundamentally different read models (a dashboard, a search index, a recommendation engine, a compliance report) and each read model has its own projection logic.

”We need to react to state changes”

Domain events without event sourcing solve this. Publish events when state changes, but keep the database as the source of truth. The events are notifications, not the authoritative record.

// Simpler alternative: domain events with CRUD storage
@Transactional
public void placeOrder(PlaceOrderCommand command) {
    Order order = new Order(command);
    orderRepository.save(order); // CRUD save

    // Publish event as notification
    eventPublisher.publish(new OrderPlacedEvent(order.getId(), order.getTotal()));
}

When to use event sourcing instead: when the events must be the authoritative record, not a derived notification. When you need guaranteed event ordering, replay capability, and the ability to derive new read models from historical events.

The Decision Framework

Ask these five questions before adopting event sourcing:

1. Is the event history itself valuable, or do you only need the current state?

If you only need the current state plus an audit trail, use CRUD with an audit log. If the event history is a first-class business asset (financial transactions, legal proceedings, medical records), event sourcing may be justified.

2. Do you need to derive multiple, fundamentally different read models from the same data?

If you have one read model (or read models that differ only in filtering/sorting), CQRS with database views or read replicas is sufficient. If you need to compute a real-time dashboard, a full-text search index, a machine learning feature store, and a regulatory report from the same events, event sourcing provides the foundation.

3. Can your team afford the operational overhead?

Event sourcing requires expertise in distributed systems, eventual consistency, message brokers, and schema evolution. If your team has three developers building a content management system, the operational overhead will consume more time than the features. If your team has dedicated infrastructure engineers and the domain complexity justifies it, the overhead is manageable.

4. Is eventual consistency acceptable for your use case?

In CQRS with event sourcing, the read side is eventually consistent with the write side. If your domain requires immediate consistency (user updates their email, immediately sees the new email on the next page), event sourcing with CQRS adds complexity for a consistency guarantee you must work around. You can mitigate this (read-your-writes, synchronous projections), but each mitigation adds complexity.

5. Are you building a new system or modifying an existing one?

Event sourcing is easier to adopt for a new system than to retrofit onto an existing one. Retrofitting requires migrating historical data into events, which is the topic of the next chapter. If the existing CRUD system works and the only motivation is architectural elegance, the migration cost is rarely justified.

Systems Where Event Sourcing Causes Harm

Simple CRUD applications. A blog, a content management system, a settings page, a user profile editor. These systems have straightforward state management, no complex business rules, and no need for event history. Event sourcing adds infrastructure and complexity with no compensating benefit.

High-throughput, simple-state systems. A URL shortener, a counter service, a rate limiter. These systems process millions of requests but each request is a simple state mutation. The event store becomes a bottleneck, and the events carry no business insight.

Systems with complex, mutable graphs. A social network’s friend graph, a file system, a dependency tree. These structures are naturally represented as mutable state with relationships. Modeling them as event streams requires encoding every relationship change as an event, and replaying the stream to compute the current graph is expensive.

Systems where the team lacks distributed systems experience. Event sourcing introduces distributed systems problems: eventual consistency, message ordering, idempotency, exactly-once processing, saga coordination. If the team has not solved these problems before, they will solve them for the first time in production.

Partial Adoption

Event sourcing does not have to be all-or-nothing. In a system with five bounded contexts, perhaps only one (the order management context) benefits from event sourcing. The other four (user accounts, product catalog, inventory counts, notification preferences) work perfectly well as CRUD.

The boundary between event-sourced and CRUD contexts is the integration events published through the message broker. The order management context publishes OrderPlaced, OrderShipped events. The notification context consumes these events and sends emails. The notification context does not need to be event-sourced. It receives events, performs actions, and stores its own state however it wants.

This is the pragmatic approach: apply event sourcing where it provides the most value, and use simpler patterns everywhere else.

Returning to the Opinions

Chapter 1 stated four opinions. After fifteen chapters of implementation, they bear revisiting:

“Event sourcing is an infrastructure pattern, not an architecture.” The infrastructure complexity this book has covered (event stores, projections, outbox relay, sagas, upcasting, partitioning, monitoring) confirms this. Event sourcing does not improve your domain model. It changes how you persist and distribute state changes. The domain model should be the same whether you persist it as events or as rows.

“CQRS does not require event sourcing.” Chapter 3 demonstrated this explicitly. A read-optimized view backed by a CRUD write model gives you CQRS without event streams, projections, or eventual consistency. This is the simpler path for most systems.

“Frameworks accelerate delivery but hide the mechanics.” Axon Framework simplifies saga management, projection tracking, and upcasting. It also hides the PostgreSQL queries, the Kafka consumer configuration, and the concurrency model. When something breaks, you need to understand the mechanics. This book built everything from scratch before showing the framework equivalents so that the mechanics are not hidden.

“The event store is PostgreSQL until proven otherwise.” PostgreSQL handles the event-sourced workload (append-only writes, stream reads, global position ordering) reliably until you exceed roughly 50,000 events per second sustained. Beyond that threshold, EventStoreDB or a sharded solution becomes necessary. Most systems never reach that threshold.

The honest conclusion: event sourcing is powerful, complex, and often unnecessary. Use it when the domain demands it. Use CQRS without event sourcing when you need read/write scaling. Use plain CRUD when neither is needed. The best architecture is the simplest one that solves the actual problem.