Skip to content

Getting a Single Instance

When you need one read model and you have its key, ask Chronicle for it by key. You always call the same operation; what differs is where the answer comes from.

Chronicle knows whether the read model is materialized — whether an observer writes its state to a sink. If it is, the kernel reads the stored instance and releases any PII before returning it. If it is not — a passive read model has no observer and therefore no sink — the kernel rebuilds the instance from the events that feed it. You do not choose between the two at the call site; the read model’s own definition decides.

The client sends the read model type and key to the kernel. The kernel resolves the read model’s definition and takes one of two paths:

  • Materialized read model — the kernel looks the instance up in the sink by key, releases compliance-protected values, and returns it. The cost is a single storage lookup regardless of how long the event history is. The instance is as current as the observer that wrote it, so it is eventually consistent.
  • Passive read model — the kernel replays the relevant events through the projection or reducer that owns the read model and returns the result. The instance reflects every event appended up to that call, so it is strongly consistent. The cost grows with the length of the history.

The materialized path is also the only one that sees the whole picture for a projection that joins across event sources or resolves keys from other events. Replay walks one event source, so it cannot reconstruct state that was built from events belonging to a different key.

The read model type must already be registered by the client, either as a model-bound projection, a declarative projection, or the read model produced by a reducer.

AccountInfo? account = await eventStore.ReadModels.GetInstanceById<AccountInfo>(accountId);
if (account is not null)
{
Console.WriteLine($"{account.Name}: {account.Balance:C}");
}

Each client names the operation in its own idiom, but the inputs are the same: the read model type and the read model key.

If no instance exists for that key, clients return their normal “not found” shape: null, nil, or an empty result depending on the language. Treat that as “no state exists yet,” not as a projection failure. For a materialized read model this covers both a key that was never created and one that has been removed — the observer is authoritative, so Chronicle does not fall back to a replay that would resurrect it.

If the read model type itself is unknown, that is a configuration error. Register the projection, reducer, or read model before querying it.

Because the read model’s definition decides the path, choose when you design the read model rather than when you query it.

Leave the read model materialized — the default — when:

  • The same data is read often, or read from lists, dashboards, and user interfaces.
  • The event history for a key can grow long.
  • A brief lag between an append and the read reflecting it is acceptable.
  • The projection joins events across event sources.

Mark the read model passive when:

  • A command needs the exact current state before making a decision.
  • You append an event and immediately need to read the state it creates.
  • The event history for each key stays short.
  • The read model is queried rarely relative to how often its events are appended.