Marcio Cunha

Event Sourcing: How to Store Changes as a Sequence of Events

Discover how event sourcing replaces traditional database tables with an immutable historical log of facts, transforming how complex systems handle data persistence and auditing.

Marcio Cunha12 min
Also available in:EspañolPortuguês
Summary
  • Event-driven storage preserves every state alteration as an immutable historical fact, eliminating the irreversible data loss typical of destructive updates.
  • Rebuilding the current state of a domain object occurs through the sequential reading of all past events, a concept known as state reconstitution.
  • Distributed systems gain superior resilience because the event log natively serves as a complete audit trail and messaging backbone.
  • Operational complexity increases considerably, requiring mature schema versioning strategies and optimized projections for fast queries.
  • The model perfectly decouples data writes from read requirements, enabling multiple specialized analytical databases without impacting the core application.

The Fundamental Flaw of Traditional Databases

When building enterprise applications, the standard approach consists of storing the current state of entities in relational tables or document stores. If a customer changes their address, the system executes an instruction that overwrites the old data with the new one. In practice, this means the past of that record disappears forever, leaving only a snapshot of the present moment. This destructive model works well for simple systems, but it fails miserably when we need to answer complex temporal questions, such as what a bank account balance was exactly last Tuesday or why a specific order was canceled.

The loss of historical data complicates compliance audits and prevents the analysis of behavioral trends over time. This exact scenario is where event sourcing emerges as a radical and powerful alternative. Instead of saving only the final snapshot, the architecture records every state change as an independent, immutable event. An event represents a fact that already occurred in the past, such as 'OrderCreated', 'PaymentApproved', or 'AddressUpdated'. By accumulating these facts in sequence, we create an inexhaustible source of context and traceability for any modern system.

The Mechanics of Event Sourcing: Facts Instead of Snapshots

To understand the practical functioning of this approach, imagine a bank statement. The bank does not store only your current balance statically; it maintains a list of every transaction, deposit, and withdrawal you have ever made. The current balance is always the mathematical result of summing those accumulated events. In software development, we apply this exact business logic to complex entities like shopping carts, user accounts, or insurance policies.

When an operation occurs, the system validates the user's intent against the current state and, if everything is valid, generates a new event. This event is appended sequentially to an immutable log file known as the Event Store. Because the write operation is strictly sequential, the database performs extremely fast write operations known technically as append-only. In practice, this means records are never altered or deleted, guaranteeing absolute integrity of the stored data and eliminating destructive concurrency disputes.

Rebuilding Current State Through Sequential Reading

A common question from newcomers to this architecture is how the application can display the current state of an object if we only save isolated pieces of history. The process that solves this question is called state reconstitution. Whenever a user requests information about a profile or an order, the persistence engine fetches all events associated with that specific identifier from the database and executes them one by one in the exact chronological order they occurred.

To optimize this process and prevent the system from processing thousands of old events every time someone opens a screen, we use snapshots. A snapshot is a consolidated record of the entity's state at a specific point in time. Thus, instead of reading from the first event generated five years ago, the application loads the latest available snapshot and processes only the few events that occurred after it. In practice, this drastically reduces memory consumption and keeps application response speeds excellent, even for entities with extremely long histories.

Decoupling Writes and Reads with Projections and CQRS

Separating how data is written from how it is queried is one of the greatest triumphs provided by this architecture. This pattern often goes hand in hand with CQRS, which stands for Command Query Responsibility Segregation. In simple terms, we create separate paths for modifying data (commands) and reading data (queries). While the write side focuses exclusively on validating business rules and generating events, the read side feeds optimized databases tailored for text search, reports, or charts.

These read databases are updated by components called projectors, which listen to generated events in real time and build customized views called projections. If tomorrow the business needs an entirely new report with cross-referenced metrics, we simply create a new projector that consumes the same event history from the beginning, without altering a single line of legacy registration code. In practice, this grants extraordinary flexibility for engineering teams to adapt the system to new market demands with minimal effort and zero risk of corrupting transactional data.

Real-World Challenges: Complexity, Versioning, and Consistency

No architecture is a silver bullet, and adopting event-driven storage brings significant operational costs that must be weighed before any migration. The greatest challenge lies in schema versioning. Since events are immutable and saved forever, what happens when an event structure needs to change because business rules evolved? If the old application wrote an event one way and the new version expects another, the reading mechanism can break. To bypass this, developers must implement upcasting strategies, converting old events into compatible formats on the fly when read by the application.

Another critical point is eventual consistency. Unlike a traditional relational database where writes and reads occur at the exact same instant and transaction, projectors in distributed systems take a few milliseconds or seconds to process events and update read screens. This imperceptible delay for humans requires user interfaces to be designed to handle asynchronous updates smoothly. Furthermore, debugging failures requires a radical mindset shift for the team, moving from inspecting static tables to investigating temporal logs.

class ShoppingCart: def __init__(self, cart_id): self.cart_id = cart_id self.items = [] self.is_completed = False self._version = 0 def apply_event(self, event): if event['type'] == 'ItemAdded': self.items.append(event['product']) elif event['type'] == 'CartCompleted': self.is_completed = True self._version += 1 def load_from_history(self, events): for event in events: self.apply_event(event)

Final Thoughts on Model Viability and Future Outlook

Deciding to adopt this approach in a project requires rigorous domain analysis. Simple systems, traditional CRUD applications, or short-lived MVPs rarely justify the additional operational complexity of managing event streams and eventual consistency. However, for complex domains packed with strict business rules, mandatory audit trails, financial transactions, or collaborative workflows, event-driven architecture offers unmatched robustness and clarity for sustainable platform growth.

Ultimately, storing changes as event sequences reconnects us with the true essence of time and human actions within computational systems. By embracing the past as an immutable asset rather than disposable data, we build platforms capable of continuously evolving, auditing their own decisions with surgical precision, and bravely withstanding inevitable business changes over the years.