Marcio Cunha

Domain Modeling with Event Sourcing and Read Projections

Learn how to build resilient systems using Event Sourcing to record complete mutation histories and read projections optimized for fast queries.

Marcio Cunha•3 min
Also available in:EspañolPortuguês
Summary
  • Event-based storage eliminates historical context loss by persisting every change as an immutable truth.
  • Read projections decouple writes from queries, allowing databases to be optimized for specific purposes.
  • Eventual consistency requires interface compensation strategies to handle millisecond delays in data replication.
  • Rebuilding complex aggregates requires periodic snapshots to prevent drastic performance degradation.
  • Event schema evolution demands strict versioning and upcasters to ensure long-term backward compatibility.

The Challenge of Modeling Highly Dynamic Systems

In traditional software development, we usually save only the final state of a record in a relational or NoSQL database. In practice, this means that if a user changes their delivery address three times, we overwrite the previous data and lose the entire history of that journey. For complex enterprise systems, this loss of context prevents accurate audits and makes it difficult to understand how the system reached a given state.

When dealing with data-driven architectures, the need arises for an approach where the system's truth is not a static snapshot of the present, but rather the video tape containing all past occurrences. This is where fact-based modeling comes into play, allowing every modification to be tracked transparently and immutably over time.

Fundamentals of Event Sourcing and Fact Storage

Event sourcing consists of storing every state change in an application as a chronological sequence of immutable events, known as an Event Store. In practice, instead of keeping a table row with a bank account's current balance, we keep a list containing every deposit and withdrawal made. Each event represents a fact that already happened in the past and cannot be altered or deleted.

This approach guarantees a native and perfect audit trail, because the current system state is always calculated by recalculating or replaying these events from the beginning. Although it seems computationally expensive, the gain in traceability, debugging of complex bugs, and transactional resilience widely compensates for the initial implementation complexity.

The Critical Role of Optimized Read Projections

Replaying thousands of events every time a user opens a screen would be unfeasible from a performance standpoint. To solve this bottleneck, we use the read projection pattern, which consists of reading stored events and transforming them into denormalized views ready for consumption in separate databases, such as fast relational tables or JSON documents.

In practice, every time an event occurs, a background process updates a specific query table. Thus, write operations remain focused on the security of the event history, while read operations respond instantly to complex queries without overloading the application core.

Managing Eventual Consistency and Concurrency

In architectures that separate the write model from the read model, information does not appear everywhere at the exact millisecond, a phenomenon known as eventual consistency. In practice, this means that after a customer confirms a purchase, the order dashboard may take a few moments to display the new item in the list, requiring the user interface to handle this small latency transparently.

To avoid conflicts when multiple users modify the same record simultaneously, event storage uses optimistic concurrency control based on version numbers. If two requests attempt to write an event for the same aggregate using the same base version, the second request is rejected, forcing the application to refresh its data and try again.

Schema Evolution and Event Versioning

Software systems evolve, and over time the data making up an event needs to change to accommodate new business rules. Since events are immutable and recorded forever, we cannot simply alter the structure of an old table without breaking the reading of historical records stored years ago.

To solve this dilemma, we implement upcasters, which are translation functions executed at the moment old events are loaded. In practice, the upcaster takes an event in an outdated version and converts it at runtime into the latest format, allowing the application to process old and new data uniformly.

Final Considerations on Scalability and Architecture

The combined adoption of historical fact-based architectures and optimized reading transforms a system's capacity to scale under high demand. Although it requires a steeper initial learning curve for the development team compared to traditional CRUD models, the operational benefits far outweigh the costs. By clearly separating where data is securely written from where it is queried at high speed, we build robust technology foundations prepared for continuous business growth.