Projection With Initial Values
Initial values define the read model state Chronicle starts from before applying event mappings. Use them when a property needs a meaningful default even though no event has assigned it yet.
The exact API name depends on the client. .NET exposes WithInitialValues, while TypeScript exposes withInitialValues.
Basic Usage
Section titled “Basic Usage”Provide a factory that returns a fresh read model instance with the desired default values. Event mappings then apply on top of that initial state.
Kotlin and Elixir don’t have a WithInitialValues-style builder call, but they reach the same result idiomatically: as the client builds the projection definition it registers, it reads the read model type’s own declared defaults — Kotlin’s primary-constructor defaults, Elixir’s defstruct defaults — and captures them into that definition (see Collections below for a working example that doesn’t need event context). That capture is why the defaults reach the stored state: they travel with the registered definition, exactly as WithInitialValues values do. Nothing runs the constructor when a document is later read — see The constructor may not run. This particular example also maps an event context property (Occurred), which neither client’s fluent projection API can access yet.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
public enum InitialValuesUserStatus{ Inactive, Active}
[EventType]public record InitialValuesUserCreated(string Name, string Email);
public record InitialValuesUserProfile( string Name, string Email, InitialValuesUserStatus Status, DateTimeOffset CreatedAt, DateTimeOffset? LastLogin, int LoginCount, bool IsVerified);
public class InitialValuesUserProfileProjection : IProjectionFor<InitialValuesUserProfile>{ public void Define(IProjectionBuilderFor<InitialValuesUserProfile> builder) => builder .WithInitialValues(() => new InitialValuesUserProfile( Name: "Unknown user", Email: string.Empty, Status: InitialValuesUserStatus.Inactive, CreatedAt: DateTimeOffset.UnixEpoch, LastLogin: null, LoginCount: 0, IsVerified: false)) .From<InitialValuesUserCreated>(_ => _ .Set(m => m.Status).ToValue(InitialValuesUserStatus.Active) .Set(m => m.CreatedAt).ToEventContextProperty(c => c.Occurred));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
enum class InitialValuesUserStatus { Inactive, Active}
@EventTypedata class InitialValuesUserCreated(val name: String, val email: String)
// Kotlin default parameter values are the read model's initial values — the kernel builds// the starting instance by calling the primary constructor with none of its arguments supplied.data class InitialValuesUserProfile( val name: String = "Unknown user", val email: String = "", val status: InitialValuesUserStatus = InitialValuesUserStatus.Inactive, val lastLogin: String? = null, val loginCount: Int = 0, val isVerified: Boolean = false)
class InitialValuesUserProfileProjection : IProjectionFor<InitialValuesUserProfile> { override fun define(builder: IProjectionBuilderFor<InitialValuesUserProfile>) { builder.from(InitialValuesUserCreated::class) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
enum InitialValuesUserStatus { Inactive, Active}
@EventTyperecord InitialValuesUserCreated(String name, String email) {}
// Field initializers are the read model's initial values — the kernel builds the starting// instance by calling the no-argument constructor.class InitialValuesUserProfile { public String name = "Unknown user"; public String email = ""; public InitialValuesUserStatus status = InitialValuesUserStatus.Inactive; public String lastLogin = null; public int loginCount = 0; public boolean isVerified = false;}
class InitialValuesUserProfileProjection implements IProjectionFor<InitialValuesUserProfile> { @Override public void define(IProjectionBuilderFor<InitialValuesUserProfile> builder) { builder.from(InitialValuesUserCreated.class); }}defmodule MyApp.Events.DecInitialValuesUserCreated do use Chronicle.Events.EventType, id: "dec-initial-values-user-created"
defstruct [:name, :email]end
defmodule MyApp.ReadModels.DecInitialValuesUserProfile do use Chronicle.ReadModels.ReadModel
# Elixir has no separate WithInitialValues builder call — the struct's own # defaults are captured as the projection's initial model state. defstruct name: "Unknown user", email: "", status: "Inactive", created_at: ~U[1970-01-01 00:00:00Z], last_login: nil, login_count: 0, is_verified: falseend
defmodule MyApp.Projections.DecInitialValuesUserProfileProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecInitialValuesUserProfile
alias MyApp.Events.DecInitialValuesUserCreated
from DecInitialValuesUserCreated, set: [ status: "$value(Active)", created_at: :occurred ]endimport { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle';
enum InitialValuesUserStatus { Inactive = 'Inactive', Active = 'Active'}
@eventType()class InitialValuesUserCreated { constructor(readonly name: string, readonly email: string) {}}
@readModel()class InitialValuesUserProfile { name = 'Unknown user'; email = ''; status = InitialValuesUserStatus.Inactive; createdAt = new Date(0); lastLogin: Date | null = null; loginCount = 0; isVerified = false;}
@projection('', InitialValuesUserProfile)class InitialValuesUserProfileProjection implements IProjectionFor<InitialValuesUserProfile> { define(builder: IProjectionBuilderFor<InitialValuesUserProfile>): void { builder .withInitialValues(() => new InitialValuesUserProfile()) .from(InitialValuesUserCreated, _ => _ .set(m => m.status).toValue(InitialValuesUserStatus.Active) .set(m => m.createdAt).toEventContextProperty('occurred')); }}Why Use Initial Values
Section titled “Why Use Initial Values”Without initial values, properties that are not touched by a projection mapping keep the platform’s default representation. That can mean null, 0, false, an empty string, or an unset date depending on the client and storage representation.
Initial values are useful for:
| Need | Example |
|---|---|
| Business defaults | A new order starts as draft until an event changes its status. |
| Non-null collections | A customer read model starts with empty address and tag collections. |
| Sentinel values | A timestamp starts at a known sentinel until the first event provides a real value. |
| Partial events | Inventory limits are configured once while stock events update only quantities. |
Business Defaults
Section titled “Business Defaults”Set defaults that describe the domain state before the first relevant event changes it.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
public enum InitialValuesOrderStatus{ Draft, Submitted}
[EventType]public record InitialValuesOrderSubmitted(string CustomerName, decimal TotalAmount);
public record InitialValuesOrderSummary( string CustomerName, InitialValuesOrderStatus Status, decimal TotalAmount, DateTimeOffset SubmittedAt, string Notes);
public class InitialValuesOrderSummaryProjection : IProjectionFor<InitialValuesOrderSummary>{ public void Define(IProjectionBuilderFor<InitialValuesOrderSummary> builder) => builder .WithInitialValues(() => new InitialValuesOrderSummary( CustomerName: string.Empty, Status: InitialValuesOrderStatus.Draft, TotalAmount: 0m, SubmittedAt: DateTimeOffset.UnixEpoch, Notes: "No notes")) .From<InitialValuesOrderSubmitted>(_ => _ .Set(m => m.Status).ToValue(InitialValuesOrderStatus.Submitted) .Set(m => m.SubmittedAt).ToEventContextProperty(c => c.Occurred));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
enum class InitialValuesOrderStatus { Draft, Submitted}
@EventTypedata class InitialValuesOrderSubmitted(val customerName: String, val totalAmount: Double)
data class InitialValuesOrderSummary( val customerName: String = "", val status: InitialValuesOrderStatus = InitialValuesOrderStatus.Draft, val totalAmount: Double = 0.0, val notes: String = "No notes")
class InitialValuesOrderSummaryProjection : IProjectionFor<InitialValuesOrderSummary> { override fun define(builder: IProjectionBuilderFor<InitialValuesOrderSummary>) { builder.from(InitialValuesOrderSubmitted::class) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
enum InitialValuesOrderStatus { Draft, Submitted}
@EventTyperecord InitialValuesOrderSubmitted(String customerName, double totalAmount) {}
class InitialValuesOrderSummary { public String customerName = ""; public InitialValuesOrderStatus status = InitialValuesOrderStatus.Draft; public double totalAmount = 0.0; public String notes = "No notes";}
class InitialValuesOrderSummaryProjection implements IProjectionFor<InitialValuesOrderSummary> { @Override public void define(IProjectionBuilderFor<InitialValuesOrderSummary> builder) { builder.from(InitialValuesOrderSubmitted.class); }}defmodule MyApp.Events.DecInitialValuesOrderSubmitted do use Chronicle.Events.EventType, id: "dec-initial-values-order-submitted"
defstruct [:customer_name, :total_amount]end
defmodule MyApp.ReadModels.DecInitialValuesOrderSummary do use Chronicle.ReadModels.ReadModel
defstruct customer_name: "", status: "Draft", total_amount: 0, submitted_at: ~U[1970-01-01 00:00:00Z], notes: "No notes"end
defmodule MyApp.Projections.DecInitialValuesOrderSummaryProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecInitialValuesOrderSummary
alias MyApp.Events.DecInitialValuesOrderSubmitted
from DecInitialValuesOrderSubmitted, set: [ status: "$value(Submitted)", submitted_at: :occurred ]endimport { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle';
enum InitialValuesOrderStatus { Draft = 'Draft', Submitted = 'Submitted'}
@eventType()class InitialValuesOrderSubmitted { constructor(readonly customerName: string, readonly totalAmount: number) {}}
@readModel()class InitialValuesOrderSummary { customerName = ''; status = InitialValuesOrderStatus.Draft; totalAmount = 0; submittedAt = new Date(0); notes = 'No notes';}
@projection('', InitialValuesOrderSummary)class InitialValuesOrderSummaryProjection implements IProjectionFor<InitialValuesOrderSummary> { define(builder: IProjectionBuilderFor<InitialValuesOrderSummary>): void { builder .withInitialValues(() => new InitialValuesOrderSummary()) .from(InitialValuesOrderSubmitted, _ => _ .set(m => m.status).toValue(InitialValuesOrderStatus.Submitted) .set(m => m.submittedAt).toEventContextProperty('occurred')); }}Collections
Section titled “Collections”Initialize collections so consumers can treat them as empty collections instead of checking for missing values.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
public record InitialValuesAddress(string Street, string City);
[EventType]public record InitialValuesCustomerRegistered(string Name);
public record InitialValuesCustomerRecord( string Name, IEnumerable<InitialValuesAddress> Addresses, IEnumerable<string> Tags);
public class InitialValuesCustomerRecordProjection : IProjectionFor<InitialValuesCustomerRecord>{ public void Define(IProjectionBuilderFor<InitialValuesCustomerRecord> builder) => builder .WithInitialValues(() => new InitialValuesCustomerRecord( Name: string.Empty, Addresses: Array.Empty<InitialValuesAddress>(), Tags: Array.Empty<string>())) .From<InitialValuesCustomerRegistered>();}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
data class InitialValuesAddress(val street: String, val city: String)
@EventTypedata class InitialValuesCustomerRegistered(val name: String)
data class InitialValuesCustomerRecord( val name: String = "", val addresses: List<InitialValuesAddress> = emptyList(), val tags: List<String> = emptyList())
class InitialValuesCustomerRecordProjection : IProjectionFor<InitialValuesCustomerRecord> { override fun define(builder: IProjectionBuilderFor<InitialValuesCustomerRecord>) { builder.from(InitialValuesCustomerRegistered::class) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
import java.util.Collections;import java.util.List;
class InitialValuesAddress { public String street; public String city;
InitialValuesAddress(String street, String city) { this.street = street; this.city = city; }}
@EventTyperecord InitialValuesCustomerRegistered(String name) {}
class InitialValuesCustomerRecord { public String name = ""; public List<InitialValuesAddress> addresses = Collections.emptyList(); public List<String> tags = Collections.emptyList();}
class InitialValuesCustomerRecordProjection implements IProjectionFor<InitialValuesCustomerRecord> { @Override public void define(IProjectionBuilderFor<InitialValuesCustomerRecord> builder) { builder.from(InitialValuesCustomerRegistered.class); }}defmodule MyApp.Events.InitialValuesCustomerRegistered do use Chronicle.Events.EventType, id: "initial-values-customer-registered"
defstruct [:name]end
defmodule MyApp.ReadModels.InitialValuesCustomerRecord do use Chronicle.ReadModels.ReadModel
defstruct name: "", addresses: [], tags: []end
defmodule MyApp.Projections.InitialValuesCustomerRecordProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.InitialValuesCustomerRecord
alias MyApp.Events.InitialValuesCustomerRegistered
from InitialValuesCustomerRegisteredendimport { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle';
class InitialValuesAddress { street = ''; city = '';}
@eventType()class InitialValuesCustomerRegistered { constructor(readonly name: string) {}}
@readModel()class InitialValuesCustomerRecord { name = ''; addresses: InitialValuesAddress[] = []; tags: string[] = [];}
@projection('', InitialValuesCustomerRecord)class InitialValuesCustomerRecordProjection implements IProjectionFor<InitialValuesCustomerRecord> { define(builder: IProjectionBuilderFor<InitialValuesCustomerRecord>): void { builder .withInitialValues(() => new InitialValuesCustomerRecord()) .from(InitialValuesCustomerRegistered); }}Events That Do Not Cover Every Property
Section titled “Events That Do Not Cover Every Property”Initial values are useful when events update operational fields but leave configuration fields unchanged.
TypeScript note: this example uses
Add()/With(), the fluent builder’s variable-delta accumulation operators. TypeScript’sadd()/subtract()are not implemented yet (FromBuilder.tsthrows at runtime even though the call type-checks), so this specific example is unsupported in TypeScript for now. Elixir’sadd:/subtract:property options do work, so this example is also shown there.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record InitialValuesStockReceived(int Quantity);
[EventType]public record InitialValuesStockReserved(int Quantity);
public record InitialValuesInventoryItem( int CurrentStock, int ReservedStock, DateTimeOffset LastUpdated, int MinimumLevel, int MaximumLevel, int ReorderPoint);
public class InitialValuesInventoryProjection : IProjectionFor<InitialValuesInventoryItem>{ public void Define(IProjectionBuilderFor<InitialValuesInventoryItem> builder) => builder .WithInitialValues(() => new InitialValuesInventoryItem( CurrentStock: 0, ReservedStock: 0, LastUpdated: DateTimeOffset.UnixEpoch, MinimumLevel: 10, MaximumLevel: 1000, ReorderPoint: 20)) .From<InitialValuesStockReceived>(_ => _ .Add(m => m.CurrentStock).With(e => e.Quantity) .Set(m => m.LastUpdated).ToEventContextProperty(c => c.Occurred)) .From<InitialValuesStockReserved>(_ => _ .Add(m => m.ReservedStock).With(e => e.Quantity) .Set(m => m.LastUpdated).ToEventContextProperty(c => c.Occurred));}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.InitialValuesStockReceived do use Chronicle.Events.EventType, id: "initial-values-stock-received"
defstruct [:quantity]end
defmodule MyApp.Events.InitialValuesStockReserved do use Chronicle.Events.EventType, id: "initial-values-stock-reserved"
defstruct [:quantity]end
defmodule MyApp.ReadModels.InitialValuesInventoryItem do use Chronicle.ReadModels.ReadModel
defstruct current_stock: 0, reserved_stock: 0, last_updated: nil, minimum_level: 10, maximum_level: 1000, reorder_point: 20end
defmodule MyApp.Projections.InitialValuesInventoryProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.InitialValuesInventoryItem
alias MyApp.Events.{InitialValuesStockReceived, InitialValuesStockReserved}
from InitialValuesStockReceived, add: [current_stock: :quantity], set: [last_updated: :occurred]
from InitialValuesStockReserved, add: [reserved_stock: :quantity], set: [last_updated: :occurred]endTypeScript does not support this workflow yet.Best Practices
Section titled “Best Practices”- Use the factory form so each read model starts from a fresh instance.
- Prefer deterministic defaults. The initial state is captured when the projection definition is built.
- Keep defaults domain meaningful; do not duplicate platform defaults unless the value communicates intent.
- Initialize collections that readers naturally expect to enumerate.
- Let events own facts that actually happened; initial values are for the state before those facts are projected.