Skip to content

Deep dive: consistency

Consistency is where event sourcing feels different from CRUD, and where most early confusion lives. The short version: writes are validated immediately; reads are usually eventual. This page unpacks that and shows how to choose the right tool for each decision.

In a CRUD system, you write a row and read it straight back — one place, one consistency model. Event sourcing splits the write side from the read side, and they have different guarantees:

append, validated now

projected shortly after

query, may briefly lag

Command

Event log

Read model

Reader

  • The write side is consistent now. When you append an event, it’s committed and ordered immediately. Invariants enforced at append time (constraints, the boundary below) hold the moment the command returns.
  • The read side is eventual. A projection updates the read model after the append. The window is usually milliseconds, but a read taken instantly after a write may not reflect it yet.

Because eventual reads are what make the model scale and stay simple: projections run independently, read models specialize per use case, and the write path doesn’t wait on every view being updated. The cost is the small lag — and most UIs handle it naturally, especially with observable queries that update the moment the projection catches up.

When you genuinely need read-after-write for a specific case, you have two tools. A passive read model is never stored: Chronicle computes it from its events when you read it, so it always includes the latest append, at the price of replaying the instance’s history — for a read model keyed by its event source id, since only that event source’s events are replayed. Or keep the read model materialized and have the one caller that needs its own write back wait for the append’s observers before reading. Neither makes other writers wait. See Read model consistency.

Protecting invariants: don’t read, constrain

Section titled “Protecting invariants: don’t read, constrain”

The classic mistake is enforcing a rule by reading a model first: “is this email taken? no → append.” Because the read is eventual, two commands can race and both pass. Enforce invariants on the write side instead:

  • A constraint rejects an append that would violate a rule (uniqueness, for example) — evaluated against authoritative state at append time.
  • A decision read guards a rule over state you read first. See the Dynamic Consistency Boundary below.

Traditional event sourcing draws the consistency boundary around a fixed aggregate. A Dynamic Consistency Boundary lets the decision define it: the append is rejected when an event the decision used changed after it was read. Chronicle gives you that for one event source, optionally narrowed to one stream and to the event types the decision reads. A plain read of a read model, even a fresh one, guards nothing. Choosing how a rule holds shows which mechanism fits which rule, and Dynamic Consistency Boundary in Chronicle says what maps and what does not.

You need…Use
A view to read and renderA normal projection — embrace eventual consistency
The UI to update as data changesAn observable query
A unique value, or something created onceA constraint
A rule over state that must hold while writers raceA decision read, see DCB
Read-after-write for one specific caseA passive read model, or waiting for the append’s observers