Skip to content
LogluneStart free

Event Sourcing: Store Events, Rebuild Past State

By Loglune

Immutable events are replayed in order to rebuild a feature flag's state at a past time.
Where the state comes from
CRUD table
The current row, overwritten on every update
Event sourcing
An append-only log of events, replayed into state
Loglune
Flag change events, replayed into the version live at time T

Event sourcing is a design pattern that stores every change to an application’s state as an immutable event in an append-only log, instead of overwriting the current state in a table. Replaying the events from the start rebuilds the current state, and replaying them up to a past time T rebuilds the state at T.

This guide is for developers deciding whether event sourcing is worth its costs in a system that needs an audit trail or answers about past states. The pattern fits when history is part of the product, and it costs you event schema evolution, eventual consistency in read models, and the work of replaying long event streams. After reading, you can judge whether those costs fit your system, and you can see the same rebuild-at-T idea applied to a feature flag in a real run of Loglune, a feature-flag platform that stores flag edits as change events. The guide does not cover language- or cloud-specific implementations such as C# or AWS.

Reproduce a customer’s flag state

Start free

How does event sourcing work?

An event-sourced system records what happened as a sequence of events and derives the state from that sequence. The pattern has four parts.

  1. A command asks for a change, for example “reserve two seats” or “change the targeting rule of flag new-checkout”.
  2. The application checks the command against the current state and, if the command is valid, appends one or more events, such as SeatsReserved or FlagUpdated, to the event store. An event describes a fact in the past tense and is never edited after it is written.
  3. The event store keeps each entity’s events in order, usually with a sequence number and a timestamp on every event. Give each event an incremental identifier so that the store can reject two concurrent writes for the same entity.
  4. Readers get state by replaying events. Replaying an entity’s events in order from its initial state produces the entity’s current state, and replaying the events up to a sequence number or a time produces the state at that point. The store that holds these streams is the event store: a database that keeps events in an append-only stream per entity and reads them back in order. It can be a purpose-built event database or a relational or document database with an append-only table.

Martin Fowler’s Event Sourcing article from 2005 is the original description of the pattern and frames the idea as capturing every change to application state in an event object. Chris Richardson’s Pattern: Event sourcing on microservices.io describes the same idea for a business entity such as an Order, persisted as a sequence of state-changing events.

Events vs state: what event sourcing stores

A conventional CRUD table keeps the current row, overwritten on every update. The table answers “what is the value now” and loses “what was the value before” unless you build a separate history table.

An event store keeps an append-only log of events, replayed into state, so it keeps the changes themselves. The difference shows up in three questions.

Question CRUD table Event store
What is the value now? Read the row Replay the entity’s events, or read a projection
What was the value on September 4? Lost, unless a history table exists (a temporal database keeps it for you) Replay the events up to September 4
Who changed the value, and from what to what? Lost, unless an audit table exists Each event records the change, and usually the actor and the time

Events should capture business intent. An event that says two seats were reserved explains what happened, while an event that says the remaining seat count changed to 42 records the resulting state and loses the reason. Intent-focused events give more useful audit trails and let you build new read models from old events later.

How do you rebuild state from events?

You rebuild state by folding the events over an initial state: start from empty, apply each event in sequence order, and stop at the end of the stream or at the point you want. Two techniques make this practical.

Projections

A projection is a read model built by replaying events into a shape that is convenient to query, such as a table of current orders or a list of flags with their current rules. Building a projection this way is rebuilding the event-sourced state, and it makes historical queries possible: the same events can be replayed to a past point to answer what the state was then. Projections can be thrown away and rebuilt from the events, because the events, not the projection, hold the facts.

Snapshots

A snapshot is a stored copy of an entity’s state at a known position in its event stream. To rebuild the entity, load the latest snapshot and replay only the events written after it. Take snapshots at intervals such as every N events. Snapshots are an optimization: the event stream stays the source of truth, and snapshots can be regenerated from it. Snapshots sit next to projections and aggregates as core concepts of event sourcing.

A snapshot is a cached state at one position in the stream, never the source of truth: it can be deleted and rebuilt from the events. Snapshots trade storage for replay time, so take them when replaying a stream is measurably slow, not by default.

Example: rebuilding a feature flag’s state at time T

Feature flag configuration is a small, concrete case of event sourcing. Every edit to a flag’s targeting rules is a change event, and the question “what did this flag serve last Thursday” is a historical query. Loglune stores flag edits this way: it replays flag change events into the version live at time T. The change event history is the source of truth, the current state is rebuilt from the whole history, and the state at time T is rebuilt from the history up to T. Edits are appended and never updated, and the same input always rebuilds the same version. Publishing turns the current draft into an immutable version with a number that increases within the workspace.

The run below is a local run of Loglune’s shipped flag store and console code on workerd’s local D1 and R2 under Vitest, on October 6, 2026, with an editorial example flag, not a customer case. The flag new-checkout serves true to accounts whose plan attribute is in a list, and the run made four edits to that list:

  • September 1: create the flag with pro and enterprise, then publish.
  • September 3: narrow the list to enterprise, then publish.
  • September 5: widen the list to pro, enterprise, and team, then publish.
  • September 6: narrow the list to team and leave the edit as an unpublished draft.

The run then listed the change events, rebuilt the version live at 2026-09-04T12:00:00.000Z twice, rebuilt the versions live at a later and an earlier time, and tried to update a stored event. The output below is copied word for word from what the run printed.

publish version=1 publishedAt=2026-09-01T10:00:00.000Z
publish version=2 publishedAt=2026-09-03T10:00:00.000Z
publish version=3 publishedAt=2026-09-05T10:00:00.000Z
--- change history (audit): seq | at | op | target | plans before -> after | first published in
1 | 2026-09-01T09:00:00.000Z | create | new-checkout | null -> ["pro","enterprise"] | 1
2 | 2026-09-03T09:00:00.000Z | update | new-checkout | ["pro","enterprise"] -> ["enterprise"] | 2
3 | 2026-09-05T09:00:00.000Z | update | new-checkout | ["enterprise"] -> ["pro","enterprise","team"] | 3
4 | 2026-09-06T09:00:00.000Z | update | new-checkout | ["pro","enterprise","team"] -> ["team"] | null
audit rows=4 chain={"verified":true,"count":4,"legacy":0,"brokenAtSeq":null,"head":{"seq":4,"hash":"0fb73b5d0c2aa2d7a4c250eacac1f6664933242f0c63ae4f31936b2b01f506de"},"anchor":"absent"}
--- projection at T=2026-09-04T12:00:00.000Z
version=2 publishedAt=2026-09-03T10:00:00.000Z
new-checkout plans=["enterprise"] default=false definitionKeys=flag,segments
same T resolved twice identical=true
--- projection at T=2026-09-07T00:00:00.000Z: version=3 plans=["pro","enterprise","team"]
--- projection at T=2026-08-31T00:00:00.000Z: null (no version live)
--- UPDATE flag_events: D1_ERROR: flag_events is append-only: SQLITE_CONSTRAINT (extended: SQLITE_CONSTRAINT_TRIGGER)

The output maps to the event sourcing concepts above.

  • The event log: each change event has a sequence number, a time, an operation, a target, and the values before and after. The audit list adds the first version that published each event. Event 4 shows null because it is still a draft, so no production version ever served it.
  • The state at T: at 2026-09-04T12:00:00.000Z, the version live was version 2, rebuilt from events 1 and 2, and the flag served true to enterprise accounts and false to pro accounts. A CRUD table that held the September 6 value ["team"] would have no record of that.
  • Determinism: rebuilding the same T twice gave identical results.
  • No state before the first event: at 2026-08-31T00:00:00.000Z, no version existed yet, so the result is null instead of a guess.
  • Append-only storage: the database refused an UPDATE on the event table with flag_events is append-only. Loglune’s change history rejects updates and deletes and links each row in a hash chain, which the audit check reported as "verified":true.

Undoing a change follows the same rule as compensating events: Loglune’s rollback and revert both append a new edit instead of removing the old one. Rolling back to version N in Loglune appends an edit that sets the draft back to version N’s content and publishes it as a new version, instead of pointing back at the old version. Reverting a single change event by its sequence number appends one edit that restores the state just before that event. The history keeps the mistake and the fix.

Event sourcing and CQRS

CQRS (Command Query Responsibility Segregation) separates the model that handles writes from the model that answers reads. Event sourcing pairs naturally with CQRS: the write side appends events, and the read side serves projections built from those events. Event sourcing is commonly combined with CQRS by materializing views from the stored events, so that reads and writes scale independently.

The two patterns are independent. You can use CQRS with an ordinary database on the write side, and you can use event sourcing with reads that replay events directly, without separate read models. In the Loglune run above, writes are edits appended to the change history, and reads are the audit list and the projection of a version, which never change the stored events.

Event sourcing vs event-driven architecture

Event-driven architecture is about communication: services publish events and other services react to them, often through a broker such as Apache Kafka. Event sourcing is about storage: one service keeps its own state as a log of events. A system can be event-driven without event sourcing, by publishing events while storing state in ordinary tables, and a single service can be event-sourced without publishing anything to other services.

Do not treat a message broker as an event store. Brokers such as Kafka usually lack per-entity stream reads and optimistic concurrency, so they work well to fan out events to projections and other consumers but do not replace an event store. Kafka can therefore feed an event-sourced system, but it should not be the event store itself.

Benefits of event sourcing

  • Complete audit trail. Every change is stored with its order and, usually, its actor and time, so “who changed what, and when” is a query, not a reconstruction from logs.
  • Historical queries. Replaying events up to a time T answers what the state was at T, which helps incident management and reproducing a bug that only one customer sees.
  • New read models from old data. A projection added today can be built from every event since the start.
  • Append-only writes. Writes add rows and never overwrite existing ones, so a later change cannot erase an earlier one.
  • Debugging by replay. Replaying the same events into the same code reproduces the same state, which turns “it was different yesterday” into a test case.

Drawbacks and challenges of event sourcing

  • Event schema evolution. Stored events cannot be edited, so a change to an event’s shape needs tolerant deserialization, version identifiers on events, or upcasters that convert old events while reading. Rewriting stored events is a last resort, because it undermines the audit trail.
  • Eventual consistency. Projections lag behind the events they are built from, so a read right after a write may not show the write yet.
  • Querying. An event store answers “give me the events of entity X”, not arbitrary queries across entities, so most queries need a projection.
  • Replay cost. Long streams take time to replay, which is the reason for snapshots.
  • Bad events stay. A bug that wrote wrong events leaves those events in the store, and the fix is a compensating event or special handling during replay. Undoing a change works the same way: you append a compensating event that reverses or corrects the earlier event, and the earlier event stays in the stream.
  • Idempotent consumers. Event delivery to consumers is usually at least once, so handlers that update projections or send emails must handle duplicates.
  • Testing style. Write given-when-then tests: set up past events, issue a command, and assert on the new events. Projections and schema upgrades need integration tests on top.

When should you use event sourcing?

Use event sourcing when the history is part of the product or of how you operate it:

  • You must show who changed what and when, for compliance, finance, or configuration that affects customers.
  • You need to answer what the state was at a past time, for example to reproduce a customer’s bug as it happened.
  • You expect to build new read models from past activity.
  • The domain already speaks in events, such as orders placed, payments captured, and seats reserved.

Avoid event sourcing when the data is simple CRUD with no need for history, when the team has no experience with eventual consistency, or when the history requirement is small enough for a history table or an audit log next to ordinary tables. The two main costs are the complexity of designing the domain model and the event log, and the storage needed when every state change is kept as an event.

Applying event sourcing to feature flag state

A feature flag’s targeting rules change often and decide what each customer sees, which makes flag configuration a strong fit for an event-sourced history. When a bug report says a customer saw the old checkout last Thursday, the question is a historical query: which version was live at that time T, and what did it serve. Loglune keeps every flag edit as a change event, rebuilds the version live at T from the events up to T, and shows it as a read-only projection next to the audit list of the changes that led to it.

FAQ

What is the difference between an event store and a message broker?

An event store keeps each entity’s events in order and rejects conflicting concurrent writes, while a broker delivers events to consumers. Loglune’s change history is an event store of the relational kind: a table that refuses updates and deletes.

Does reading flag state at a past time count toward Loglune billing?

The projection of the version live at time T, the diff between two versions, and the audit list of change events are read operations and do not count toward billing. A reproduction from time T, which rebuilds the state at T and delivers it for reproduction, counts as one reproduction, and the Free plan includes 5.

Rebuild your feature flag state at time T with event sourcing

Loglune keeps every flag edit as a change event in an append-only history and treats that history as the source of truth. The current flag state is rebuilt from the whole history, and the state at time T is rebuilt from the events up to T, the same way for the same input. The projection of the version live at T and the audit list of change events are read operations and do not count toward billing, and the Free plan keeps 7 days of history.