Marcio Cunha

Domain Modeling with Event Sourcing and CQRS Projections in Event-Driven Databases

Learn how to build resilient architectures using Event Sourcing and CQRS projections to model complex business domains and ensure consistency in distributed systems.

Marcio Cunha•4 min
Also available in:EspañolPortuguês
Summary
  • Recording user intent as an immutable sequence of facts preserves the exact operational history without audit losses.
  • Separating write operations from reads through optimized projections eliminates performance bottlenecks in high-volume queries.
  • Rebuilding the current system state via continuous event replay requires smart snapshot strategies to prevent latency.
  • Ensuring eventual consistency between write models and reads demands robust handling of duplicate messages and reorderings.
  • Adopting this architectural approach compensates for increased operational complexity only in highly intricate and dynamic business domains.

The Challenge of Changing How We Store Data

In traditional software engineering, we are accustomed to viewing the database as an instant photograph. If a user updates their delivery address, the system overwrites the old information with the new one. In practice, this means we lose all context of why and when the change happened. For highly complex business domains, this loss of history can be costly in terms of auditability, traceability, and conflict resolution. This is where immutable fact-based modeling comes into play.

Instead of saving only the current state, the Event Sourcing approach consists of recording every state change as an immutable event over time. Think of it like a bank account statement: the balance is not a static number stored in a table, but the calculated result of all deposits and withdrawals since the account was opened. When we apply this logic to software, we ensure that the system's past is as important and accessible as the present, opening doors for retroactive analysis and safe bug fixing.

The Anatomy of a Business Event

To build an event-driven system, we must change our mental model of what a data entity is. Instead of classes holding mutable properties, we think of facts that have already occurred in the past, always written in the past participle (for example: OrderPlaced, PaymentApproved, or ItemRemoved). Each event carries its own payload, which is the strict data package needed to describe exactly what changed at that specific instant in time.

In practice, these events are accumulated in a linear log, often called an event log or event store. No data is deleted or updated after it is written; operations are strictly append-only. This brings a colossal advantage for write scalability, since sequential disk writes are extremely fast. However, to display the current state to the user on the screen, we would need to recalculate everything from day one, which leads us directly to the need for separation.

Decoupling Read and Write with CQRS Projections

When readers and writers share the same data structure, concurrency problems and performance bottlenecks arise. The CQRS pattern (Command Query Responsibility Segregation) solves this by splitting the application into two completely independent paths: one side focused exclusively on receiving commands and generating events, and another side focused on reading already processed data.

Projections are the engines that translate raw write events into optimized read tables. In practice, when a CustomerRegistered event is triggered, an asynchronous consumer reads this event and updates a relational or NoSQL database custom-built for that specific query on the customer's screen. If reporting needs change tomorrow, we do not need to alter the core system structure; we simply create a new projection that listens to the same events and builds a brand-new table from scratch.

Managing the Complexity of Snapshots

One of the biggest practical challenges for those adopting continuous event logging is startup performance. If a shopping cart has accumulated ten thousand events over three years, the system will need to read and reprocess all ten thousand records every time the user wants to see the current cart. To avoid CPU and memory bottlenecks, we use a technique called snapshotting.

A snapshot works like a periodic restore point. Every one hundred processed events, for example, the system calculates and saves the consolidated state of the entity in an auxiliary record. When the application needs to rebuild the current state, it does not read from the beginning of time; it loads the latest available snapshot and processes only the events generated after it. This drastically reduces startup time and keeps the system agile even under high operational volume.

<!-- Conceptual example of projection and event handling -->

class CartProjection: def __init__(self): self.items = {} self.total = 0.0 def on_item_added(self, event): product_id = event['product_id'] quantity = event['quantity'] price = event['price'] self.items[product_id] = self.items.get(product_id, 0) + quantity self.total += price * quantity def on_item_removed(self, event): product_id = event['product_id'] price = event['price'] quantity = event['quantity'] if product_id in self.items: self.total -= price * quantity del self.items[product_id]

Eventual Consistency and Conflict Handling

In distributed systems that separate writes and reads, data consistency stops being immediate and becomes eventual. This means that at the exact millisecond a command is accepted, the corresponding read table might not have received the projection update yet. For the end user, this requires careful UI handling to avoid the impression that the system is failing or outdated.

Additionally, optimistic concurrency control becomes mandatory. Since multiple users might try to modify the same resource simultaneously based on the same past state, the event store must reject writes if the expected aggregate version does not match the current version in the database. This rejection forces the application to reprocess the business logic with the latest data before trying to save again.

Final Considerations on Event-Driven Architectures

Adopting event-based modeling and separate projections is not a silver bullet. The operational complexity of maintaining a message broker, ensuring consumer idempotency, and debugging asynchronous flows requires technical maturity from the engineering team. However, for complex domains where strict auditing, reporting flexibility, and granular scalability are critical requirements, this architecture offers a solid and lasting foundation for product growth.