Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›System Design›CQRS and Event Sourcing

Architecture Patterns

CQRS and Event Sourcing

Command Query Responsibility Segregation (CQRS) and Event Sourcing are architectural patterns that separate write operations from read operations to optimize system scalability and data integrity. By treating state changes as a sequence of immutable events, these patterns provide a robust audit trail and allow for independent scaling of query models. You should adopt these patterns in complex domains where high-concurrency writes, extensive audit requirements, or disparate read/write performance demands necessitate decoupling the data access layers.

The Fundamental Split: Command vs Query

At its core, CQRS challenges the traditional 'Create, Read, Update, Delete' (CRUD) approach where a single data model serves both users reading information and systems writing data. In complex applications, the requirements for writing data—often involving business validation, security checks, and transactional integrity—diverge significantly from the requirements for reading data, which prioritize performance, latency, and complex aggregation. By splitting the stack into a Command side (the write side) and a Query side (the read side), we can optimize each for its specific goal. The Command side can focus on business invariants and domain modeling, ensuring that state transitions are always valid. Meanwhile, the Query side can be structured as an optimized view, perhaps denormalized or stored in a completely different database technology, specifically indexed to serve the user's view models without requiring complex joins at runtime. This separation ensures that an increase in reporting or search demand does not degrade the performance of the core transaction processing system.

class AccountCommandService:
    def update_balance(self, account_id, amount):
        # Validate business invariant: account cannot go below zero
        if amount < 0: raise ValueError("Invalid amount")
        # Update persistent store
        print(f"Updating database for account {account_id}")

class AccountQueryService:
    def get_summary(self, account_id):
        # Read from an optimized read-only replica or projection
        return {"account_id": account_id, "balance": 100} # Simplified

Introduction to Event Sourcing

While CQRS addresses the separation of concerns, Event Sourcing fundamentally changes how we store state. Instead of saving only the current value of an object (the 'snapshot' approach), we store the entire history of actions that led to that current state. These actions are captured as immutable 'Events'. Every modification is an append-only operation, making the system inherently reliable for auditing and recovery. Because the events represent facts that have occurred in the past, they cannot be changed, only corrected by appending new events. This design shift turns the application into a historical ledger. If you need to know why an account balance is $50, you don't look at a column labeled 'balance'; you replay the history of 'Deposit' and 'Withdrawal' events. This provides a total audit trail and the unique capability to 'travel back in time' to see what the state of the system looked like at any specific point in the past.

def process_event(event, current_state):
    # Reconstruct state by replaying events
    if event['type'] == 'DEPOSIT':
        return current_state + event['amount']
    if event['type'] == 'WITHDRAWAL':
        return current_state - event['amount']
    return current_state

events = [{'type': 'DEPOSIT', 'amount': 100}, {'type': 'WITHDRAWAL', 'amount': 20}]

Synchronizing Through Projections

Once the system starts storing events, we face a new problem: how do we efficiently query the current state if we only have an endless stream of events? This is where 'Projections' enter the picture. A projection is a process that listens to the event store and updates a dedicated read database or cache whenever a new event is appended. This is effectively an asynchronous synchronization mechanism. The read side remains decoupled from the write side, often following an 'eventual consistency' model. Because the projection happens in the background, the user experience is high-performance, though they might see slightly stale data for a few milliseconds after an update. However, this is a conscious trade-off that allows the write side to remain extremely fast, as it only needs to validate the command and persist the event, rather than calculating a complex join for the query model.

class ProjectionEngine:
    def on_event(self, event):
        # Update read model table based on event
        if event['type'] == 'DEPOSIT':
            print("Updating balance table in read-db")
            
engine = ProjectionEngine()
engine.on_event({'type': 'DEPOSIT', 'amount': 50})

Handling Consistency and Concurrency

In distributed systems, the gap between writing an event and updating the read projection creates a challenge known as eventual consistency. When a user submits a command to update their profile, the command succeeds, but a subsequent query might still return the old data because the projection hasn't processed the event yet. To mitigate this, developers use various techniques such as 'version checking' or 'client-side polling' to reconcile the state. Furthermore, Event Sourcing simplifies concurrency control because it avoids row-level locks on the database. Since we are always appending to the end of a log, we can use optimistic concurrency—checking the current version of the aggregate before appending—to ensure that two simultaneous updates don't conflict. If the versions don't match, the transaction simply fails, and the user can be asked to retry. This makes the system significantly more robust under high-load scenarios compared to traditional locking strategies.

def update_with_optimistic_lock(current_version, new_event):
    # Check version before appending to ensure no conflict
    if current_version == get_latest_version():
        append_to_log(new_event)
    else:
        raise Exception("Concurrency violation: state changed")

Scaling for the Future

The ultimate benefit of CQRS and Event Sourcing is the ability to scale components independently. When the read load spikes, we can add more instances of the query service and the projections without affecting the write side. Conversely, if write volume grows, we can scale the command handlers and event storage horizontally. Because the read models are generated from the event store, we can also implement 'Polyglot Persistence'. This means we can maintain multiple projections of the same data for different purposes: a graph database for social connections, a search engine index for full-text queries, and a relational database for transactional reports. We can even regenerate these projections from scratch if we discover a bug or need to change our data schema, simply by replaying the entire history of events. This architectural flexibility is unmatched by traditional monolithic architectures and makes the system extremely future-proof.

def rebuild_all_views(event_log):
    # Destroy old read models and recreate them from the source of truth
    for event in event_log:
        apply_to_search_index(event)
        apply_to_reporting_db(event)

print("Rebuilding read models from event stream...")

Key points

  • CQRS separates the data modification logic from the data retrieval logic.
  • Event Sourcing treats the application state as a sequence of immutable events.
  • Command services validate business rules and produce events as the source of truth.
  • Projections asynchronously transform the event stream into optimized query models.
  • The append-only nature of event logs allows for a perfect, auditable history of changes.
  • Eventual consistency is a common trade-off in CQRS systems that requires careful UI design.
  • Optimistic concurrency control is used to prevent state conflicts during high-concurrency writes.
  • System scalability is improved by allowing read and write components to scale independently.

Common mistakes

  • Mistake: Implementing CQRS for every service in the system. Why it's wrong: It introduces significant complexity and eventual consistency hurdles that aren't justified for simple CRUD operations. Fix: Use it only for high-scale domains with complex read/write requirements.
  • Mistake: Assuming the read model must be updated synchronously in the same transaction as the event store. Why it's wrong: This couples the read and write sides, negating the performance benefits of decoupling and creating bottlenecks. Fix: Use asynchronous projections driven by event streams.
  • Mistake: Neglecting to version events in the event store. Why it's wrong: Business requirements evolve, and failing to account for schema changes in events leads to legacy data corruption. Fix: Implement upcasting or versioned event schemas from the start.
  • Mistake: Treating Event Sourcing as a database logging mechanism rather than the system of record. Why it's wrong: If the write model state is updated independently of event replay, the event store becomes untrustworthy. Fix: Ensure the current state is always derived exclusively from the event stream.
  • Mistake: Failing to handle idempotency in event consumers. Why it's wrong: Event sourcing systems are inherently distributed, and retries are inevitable, which leads to duplicate state changes if not managed. Fix: Include unique event IDs and track processed IDs at the consumer level.

Interview questions

What is the basic definition of CQRS in system design?

CQRS stands for Command Query Responsibility Segregation. At its core, it is the architectural pattern of separating the data modification operations—the commands—from the data retrieval operations—the queries. In a traditional CRUD system, we often use the same model for both reading and writing. By segregating these, we can optimize the read path differently from the write path, allowing for independent scaling and performance tuning based on specific system requirements.

What is the primary purpose of Event Sourcing, and how does it differ from traditional state storage?

Event Sourcing is an architectural pattern where we store the entire history of state changes as a sequence of immutable events rather than just the current state of an object. In traditional systems, we overwrite the previous record when an update occurs. With Event Sourcing, we reconstruct the current state by replaying these events. The primary benefit is a perfect audit log and the ability to travel back in time to debug how a specific state was reached.

How do you compare the standard CRUD approach against an Event Sourcing approach?

In a CRUD approach, you persist the current state in a relational or document store, which is intuitive and easy to implement for simple systems but lacks historical context. Event Sourcing, by contrast, stores intent. For example, instead of updating 'Balance: 100' to 'Balance: 80', you store 'Withdrawal: 20'. The trade-off is complexity; Event Sourcing requires snapshots and complex event handling, but it provides superior auditability and the ability to derive new data projections from history later.

Explain the role of projections in a CQRS and Event Sourcing architecture.

Projections act as the materialized views of your event store. Since the event store is optimized for appending logs, it is often inefficient to query it directly for complex reports. A projection service listens to incoming events, processes them, and transforms that data into a read-optimized format, such as a search index or a flattened database table. This allows the system to remain highly performant while separating the write-heavy event store from the read-heavy delivery model.

How do you handle consistency requirements in a CQRS system?

Consistency in CQRS is typically eventual rather than immediate. Because the command side updates the event store and the query side must update its projections asynchronously, there is a delay. To manage this, we use versioning or sequence numbers within events. If a user needs immediate read-after-write consistency, we can route their specific query to the command-side model or a 'session-based' projection that tracks the latest version of the data processed by the background workers.

What are the primary challenges of implementing a distributed Event Sourcing system?

The primary challenges include managing event schema evolution, ensuring idempotent event processing, and handling out-of-order events. If you change your data schema, you must implement upcasters to transform older events to the new format. Furthermore, since components operate asynchronously, you must ensure that retrying a failed process does not result in duplicate state changes, which necessitates an idempotent design where each event has a unique identifier that the consumer can track to prevent duplicate side effects.

All System Design interview questions →

Check yourself

1. What is the primary architectural benefit of separating the read and write models in CQRS?

  • A.Eliminating the need for a database
  • B.Allowing independent scaling and optimization of read and write workloads
  • C.Ensuring that all data remains strictly consistent across the system
  • D.Automatically simplifying the application code structure
Show answer

B. Allowing independent scaling and optimization of read and write workloads
Correct: It allows scaling reads (via caches/denormalized views) differently than writes. Wrong: It does not eliminate databases; it often increases them. It creates eventual consistency, not strict consistency. It increases code complexity, not simplification.

2. In an Event Sourced system, why is the 'Event Store' considered the single source of truth?

  • A.Because it is easier to index for search queries than relational tables
  • B.Because it stores the current state of an object in a JSON blob
  • C.Because the current application state is derived solely by replaying historical events
  • D.Because it provides the lowest possible latency for read requests
Show answer

C. Because the current application state is derived solely by replaying historical events
Correct: Event sourcing relies on reconstructing state from events. Wrong: Event stores are bad for search indexing. They do not store current state, but a history of transitions. They are not optimized for read-heavy latency.

3. When is Event Sourcing most appropriate for a system design?

  • A.When you need a full audit trail and the ability to travel back in time to state
  • B.When the system is a simple web form with one user interaction
  • C.When you need the simplest implementation path with minimal infrastructure
  • D.When all your read queries require transactional 'read-your-writes' consistency
Show answer

A. When you need a full audit trail and the ability to travel back in time to state
Correct: The append-only nature of events provides a natural, immutable audit trail. Wrong: It is too complex for simple CRUD. It is not the simplest path. Eventual consistency makes 'read-your-writes' difficult to guarantee.

4. How should a projection handler manage the risk of message delivery failures?

  • A.By rolling back the entire event store to the last known good state
  • B.By implementing retries with idempotency checks to ensure events are processed exactly once
  • C.By skipping the failed event and logging it to the user
  • D.By switching the system to synchronous updates immediately upon failure
Show answer

B. By implementing retries with idempotency checks to ensure events are processed exactly once
Correct: Idempotency ensures that reprocessing the same event does not corrupt the read model. Wrong: You cannot roll back a distributed event store. Skipping data leads to inconsistencies. Synchronous updates would negate the decoupling benefits.

5. Why is 'Eventual Consistency' an inherent characteristic of CQRS/Event Sourcing?

  • A.Because the read model is updated asynchronously after the write model processes the event
  • B.Because event stores are inherently slow and cannot keep up with writes
  • C.Because it is required to prevent data loss in the write model
  • D.Because database vendors mandate it for all distributed systems
Show answer

A. Because the read model is updated asynchronously after the write model processes the event
Correct: Decoupling the write process from the read view projection necessarily means there is a lag between a write and when it is visible in the read model. Wrong: Performance isn't the primary driver for eventual consistency; the decoupling is. It's not about data loss prevention. It is not a vendor mandate.

Take the full System Design quiz →

← PreviousCircuit Breaker PatternNext →Distributed Transactions — Saga Pattern

System Design

31 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app