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.
How it works
Section titled “How it works”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.
Read by key
Section titled “Read by 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}");}val account = store.readModels.getInstanceByKey(AccountInfo::class, accountId)
if (account != null) { println("${account.name}: ${account.balance}")}import io.cratis.chronicle.IEventStore;import io.cratis.chronicle.java.BlockingEventStore;import io.cratis.chronicle.readModels.ReadModel;
@ReadModelrecord AccountInfo(String name, double balance) { AccountInfo() { this("", 0.0); }}
class ReadModelLookup { void printAccount(IEventStore store, String accountId) { var account = new BlockingEventStore(store) .getReadModels() .getInstanceByKey(AccountInfo.class, accountId);
if (account != null) { System.out.println(account.name() + ": " + account.balance()); } }}alias MyApp.ReadModels.AccountInfo
{:ok, account} = Chronicle.ReadModels.get_instance_by_id(AccountInfo, account_id)
if account do IO.puts("#{account.name}: #{account.balance}")end// Canonical read-by-key API. Use when the instance is known to exist.const account = await store.readModels.getInstanceById(AccountInfo, accountId);console.log(`${account.name}: ${account.balance}`);
// To detect a missing key, use the TypeScript nullable convenience API instead.const optionalAccount = await store.readModels.findInstanceById(AccountInfo, accountId);if (optionalAccount !== null) { console.log(`${optionalAccount.name}: ${optionalAccount.balance}`);}Each client names the operation in its own idiom, but the inputs are the same: the read model type and the read model key.
Read one stream instance
Section titled “Read one stream instance”A model declaring [EventStream<TSource>(stream)] must be read with GetInstanceById<T>(EventSourceId, EventStreamId). It keeps the source ID as its model key and folds only that source’s declared stream and requested ID. The declaration supplies the event source type as well; events written without that source type are not included. This implies passive and supplies no append guard. A key-only read refuses with ReadModelRequiresStreamScope rather than mixing streams. See Read models scoped to one event stream for the monthly-limit example and query call.
Missing instances
Section titled “Missing instances”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, not a missing instance. The kernel rejects the lookup with an error naming the requested read model identifier. Use the registered identifier rather than its container name, and register the projection, reducer, or read model before querying it.
Choosing which you get
Section titled “Choosing which you get”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.
Related topics
Section titled “Related topics”- Getting a Collection of Instances - Read or replay every instance of a read model
- Getting Snapshots - Inspect how one instance evolved over time
- Watching Read Models - React to read model changes as they happen
- Materialized Read Models - Page through sink-stored read models
- Passive projections - Keep a read model out of the sink so it is computed on demand