Projections
A projection turns the event stream into a read model. It must be idempotent, because an event can arrive more than once. Here is how Chronicler keeps that safe.
A projection reads events and writes a read model. The read side of the app queries the read model, never the event stream. A projector processes each event and updates its table.
Idempotency by a version gate
An event can arrive more than once. So a projector must be safe to run twice on the same event. Chronicler does not read first to check. It relies on the store's atomic version gate.
- An update carries
WHERE ... _version < ?, so a stale or repeated write does nothing. - An insert carries
ON CONFLICT (id) DO NOTHING, so a repeat is a no-op.
The gated write returns whether it changed a row. The handler turns that into an outcome.
// The store returns (applied bool, err). The handler reports the outcome.
applied, err := store.Upsert(ctx, row)
if err != nil {
return projection.Outcome{}, err
}
return projection.Outcome(applied), nil
Outcomes and retries
A projector's Handle returns an Outcome, one of Applied, Skipped, or Ignored. Build it with projection.Outcome, so the metrics can tell a real write from a stale skip.
When a projection depends on data that has not arrived yet, do not fail hard. Return RetryAfterErr so the event is retried later.
System columns start with an underscore
A leading-underscore column, such as _version, is system bookkeeping. The domain layer never reads it. It exists to make the write gate atomic.
What feeds this
The events come from the aggregates. That is the Event sourcing pattern.