Build a separate reducer
Use a separate IReducerFor<T> when the fold needs injected services, instance behavior during replay,
or must live apart from its model. For application code, start with model-bound attributes, then fluent
mappings, then reducer code; a self-reducing model is the default for new
reducers. Choose how to build a read model explains the
choice; this guide shows the separate-class form.
Prerequisites
Section titled “Prerequisites”Before you begin, ensure you have:
- A Chronicle-enabled application
- Basic understanding of events and event sourcing
- A read model class to reduce events into
Creating a Reducer
Section titled “Creating a Reducer”1. Define Your Read Model
Section titled “1. Define Your Read Model”First, create a record representing the state you want to compute:
public record ReducersGettingStartedOrderSummary( Guid OrderId, decimal TotalAmount, int ItemCount, DateTimeOffset LastUpdated);import io.cratis.chronicle.readModels.ReadModelimport java.time.Instant
@ReadModeldata class ReducersGettingStartedOrderSummary( val orderId: String = "", val totalAmount: Double = 0.0, val itemCount: Int = 0, val lastUpdated: Instant = Instant.EPOCH)import io.cratis.chronicle.readModels.ReadModel;import java.time.Instant;
@ReadModelrecord ReducersGettingStartedOrderSummary( String orderId, double totalAmount, int itemCount, Instant lastUpdated) {
ReducersGettingStartedOrderSummary() { this("", 0.0, 0, Instant.EPOCH); }}defmodule MyApp.ReadModels.ReducersGettingStartedOrderSummary do use Chronicle.ReadModels.ReadModel
defstruct order_id: "", total_amount: 0, item_count: 0, last_updated: ""endclass ReducersGettingStartedOrderSummary { orderId = ''; totalAmount = 0; itemCount = 0; lastUpdated: Date = new Date(0);}2. Implement the Reducer
Section titled “2. Implement the Reducer”Create a reducer by implementing IReducerFor<TReadModel> with methods for each event type:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Reducers;
[EventType]public record ReducersGettingStartedOrderCreated(Guid OrderId);
[EventType]public record ReducersGettingStartedItemAddedToOrder(decimal Price, int Quantity);
[EventType]public record ReducersGettingStartedItemRemovedFromOrder(decimal Price, int Quantity);
public class ReducersGettingStartedOrderSummaryReducer : IReducerFor<ReducersGettingStartedOrderSummary>{ public ReducersGettingStartedOrderSummary Created(ReducersGettingStartedOrderCreated @event, ReducersGettingStartedOrderSummary? current, EventContext context) => new( OrderId: @event.OrderId, TotalAmount: 0m, ItemCount: 0, LastUpdated: context.Occurred);
public ReducersGettingStartedOrderSummary? ItemAdded(ReducersGettingStartedItemAddedToOrder @event, ReducersGettingStartedOrderSummary? current, EventContext context) { if (current is null) return null; // Skip if order not created yet
return current with { TotalAmount = current.TotalAmount + (@event.Price * @event.Quantity), ItemCount = current.ItemCount + @event.Quantity, LastUpdated = context.Occurred }; }
public ReducersGettingStartedOrderSummary? ItemRemoved(ReducersGettingStartedItemRemovedFromOrder @event, ReducersGettingStartedOrderSummary? current, EventContext context) { if (current is null) return null; // Skip if order not created yet
return current with { TotalAmount = current.TotalAmount - (@event.Price * @event.Quantity), ItemCount = current.ItemCount - @event.Quantity, LastUpdated = context.Occurred }; }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport java.time.Instant
@EventTypedata class ReducersGettingStartedOrderCreated(val orderId: String)
@EventTypedata class ReducersGettingStartedItemAddedToOrder(val price: Double, val quantity: Int)
@EventTypedata class ReducersGettingStartedItemRemovedFromOrder(val price: Double, val quantity: Int)
@Reducerclass ReducersGettingStartedOrderSummaryReducer { fun created(event: ReducersGettingStartedOrderCreated): ReducersGettingStartedOrderSummary = ReducersGettingStartedOrderSummary( orderId = event.orderId, totalAmount = 0.0, itemCount = 0, lastUpdated = Instant.now() )
fun itemAdded( event: ReducersGettingStartedItemAddedToOrder, current: ReducersGettingStartedOrderSummary? ): ReducersGettingStartedOrderSummary? { if (current == null) return null // Skip if order not created yet
return current.copy( totalAmount = current.totalAmount + (event.price * event.quantity), itemCount = current.itemCount + event.quantity, lastUpdated = Instant.now() ) }
fun itemRemoved( event: ReducersGettingStartedItemRemovedFromOrder, current: ReducersGettingStartedOrderSummary? ): ReducersGettingStartedOrderSummary? { if (current == null) return null // Skip if order not created yet
return current.copy( totalAmount = current.totalAmount - (event.price * event.quantity), itemCount = current.itemCount - event.quantity, lastUpdated = Instant.now() ) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reducer;import java.time.Instant;
@EventTyperecord ReducersGettingStartedOrderCreated(String orderId) {}
@EventTyperecord ReducersGettingStartedItemAddedToOrder(double price, int quantity) {}
@EventTyperecord ReducersGettingStartedItemRemovedFromOrder(double price, int quantity) {}
@Reducerclass ReducersGettingStartedOrderSummaryReducer { ReducersGettingStartedOrderSummary created(ReducersGettingStartedOrderCreated event) { return new ReducersGettingStartedOrderSummary(event.orderId(), 0.0, 0, Instant.now()); }
ReducersGettingStartedOrderSummary itemAdded( ReducersGettingStartedItemAddedToOrder event, ReducersGettingStartedOrderSummary current) { if (current == null) return null; // Skip if order not created yet
return new ReducersGettingStartedOrderSummary( current.orderId(), current.totalAmount() + (event.price() * event.quantity()), current.itemCount() + event.quantity(), Instant.now()); }
ReducersGettingStartedOrderSummary itemRemoved( ReducersGettingStartedItemRemovedFromOrder event, ReducersGettingStartedOrderSummary current) { if (current == null) return null; // Skip if order not created yet
return new ReducersGettingStartedOrderSummary( current.orderId(), current.totalAmount() - (event.price() * event.quantity()), current.itemCount() - event.quantity(), Instant.now()); }}defmodule MyApp.Events.ReducersGettingStartedOrderCreated do use Chronicle.Events.EventType, id: "reducers-getting-started-order-created"
defstruct [:order_id]end
defmodule MyApp.Events.ReducersGettingStartedItemAddedToOrder do use Chronicle.Events.EventType, id: "reducers-getting-started-item-added-to-order"
defstruct [:price, :quantity]end
defmodule MyApp.Events.ReducersGettingStartedItemRemovedFromOrder do use Chronicle.Events.EventType, id: "reducers-getting-started-item-removed-from-order"
defstruct [:price, :quantity]end
defmodule MyApp.Reducers.ReducersGettingStartedOrderSummaryReducer do use Chronicle.Reducers.Reducer, model: MyApp.ReadModels.ReducersGettingStartedOrderSummary
alias MyApp.Events.{ ReducersGettingStartedItemAddedToOrder, ReducersGettingStartedItemRemovedFromOrder, ReducersGettingStartedOrderCreated }
@handles ReducersGettingStartedOrderCreated @handles ReducersGettingStartedItemAddedToOrder @handles ReducersGettingStartedItemRemovedFromOrder
@impl true def reduce(%ReducersGettingStartedOrderCreated{} = event, _model, context) do %MyApp.ReadModels.ReducersGettingStartedOrderSummary{ order_id: event.order_id, total_amount: 0, item_count: 0, last_updated: Map.get(context, :occurred) } end
# Skip if order not created yet def reduce(%ReducersGettingStartedItemAddedToOrder{}, nil, _context), do: nil
def reduce(%ReducersGettingStartedItemAddedToOrder{} = event, model, context) do %{ model | total_amount: model.total_amount + event.price * event.quantity, item_count: model.item_count + event.quantity, last_updated: Map.get(context, :occurred) } end
# Skip if order not created yet def reduce(%ReducersGettingStartedItemRemovedFromOrder{}, nil, _context), do: nil
def reduce(%ReducersGettingStartedItemRemovedFromOrder{} = event, model, context) do %{ model | total_amount: model.total_amount - event.price * event.quantity, item_count: model.item_count - event.quantity, last_updated: Map.get(context, :occurred) } endendimport { eventType, reducer } from '@cratis/chronicle';
@eventType()class ReducersGettingStartedOrderCreated { orderId = '';}
@eventType()class ReducersGettingStartedItemAddedToOrder { price = 0; quantity = 0;}
@eventType()class ReducersGettingStartedItemRemovedFromOrder { price = 0; quantity = 0;}
// Method names must be the exact camelCase of the event's class name -// Chronicle discovers handlers by name, not by parameter type.@reducer('', undefined, ReducersGettingStartedOrderSummary)class ReducersGettingStartedOrderSummaryReducer { reducersGettingStartedOrderCreated( event: ReducersGettingStartedOrderCreated, _current: ReducersGettingStartedOrderSummary | undefined ): ReducersGettingStartedOrderSummary { return { orderId: event.orderId, totalAmount: 0, itemCount: 0, lastUpdated: new Date() }; }
reducersGettingStartedItemAddedToOrder( event: ReducersGettingStartedItemAddedToOrder, current: ReducersGettingStartedOrderSummary | undefined ): ReducersGettingStartedOrderSummary | undefined { if (!current) return undefined; // Skip if order not created yet
return { ...current, totalAmount: current.totalAmount + (event.price * event.quantity), itemCount: current.itemCount + event.quantity, lastUpdated: new Date() }; }
reducersGettingStartedItemRemovedFromOrder( event: ReducersGettingStartedItemRemovedFromOrder, current: ReducersGettingStartedOrderSummary | undefined ): ReducersGettingStartedOrderSummary | undefined { if (!current) return undefined; // Skip if order not created yet
return { ...current, totalAmount: current.totalAmount - (event.price * event.quantity), itemCount: current.itemCount - event.quantity, lastUpdated: new Date() }; }}The event context supplies metadata such as occurrence time and event source ID. TypeScript handlers can take it as a third argument after the event and current state.
Method Signatures
Section titled “Method Signatures”Reducer methods are discovered by convention and support the following signatures:
Synchronous Methods
Section titled “Synchronous Methods”using Cratis.Chronicle.Events;
public interface IReducersGettingStartedSyncSignatures<TReadModel, TEvent> where TReadModel : class{ // Without context TReadModel? WithoutContext(TEvent @event, TReadModel? current);
// With context TReadModel? WithContext(TEvent @event, TReadModel? current, EventContext context);}import io.cratis.chronicle.events.EventContextimport io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class ReducersSignaturesOpened(val orderId: String)
@EventTypedata class ReducersSignaturesItemAdded(val amount: Double)
@EventTypedata class ReducersSignaturesClosed(val reason: String)
@ReadModeldata class ReducersSignaturesOrder( val orderId: String = "", val total: Double = 0.0, val closedAt: String = "")
@Reducerclass ReducersSignaturesOrderReducer { // (event) - the state so far is not needed fun opened(event: ReducersSignaturesOpened) = ReducersSignaturesOrder(orderId = event.orderId)
// (event, current) - current is null until the first event for an event source fun itemAdded(event: ReducersSignaturesItemAdded, current: ReducersSignaturesOrder?) = current?.copy(total = current.total + event.amount)
// (event, current, context) - context carries the event's metadata fun closed( event: ReducersSignaturesClosed, current: ReducersSignaturesOrder?, context: EventContext ) = current?.copy(closedAt = context.occurred.toString())}import io.cratis.chronicle.events.EventContext;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord ReducersSignaturesOpened(String orderId) {}
@EventTyperecord ReducersSignaturesItemAdded(double amount) {}
@EventTyperecord ReducersSignaturesClosed(String reason) {}
@ReadModelrecord ReducersSignaturesOrder(String orderId, double total, String closedAt) { ReducersSignaturesOrder() { this("", 0.0, ""); }}
@Reducerclass ReducersSignaturesOrderReducer { // (event) - the state so far is not needed ReducersSignaturesOrder opened(ReducersSignaturesOpened event) { return new ReducersSignaturesOrder(event.orderId(), 0.0, ""); }
// (event, current) - current is null until the first event for an event source ReducersSignaturesOrder itemAdded(ReducersSignaturesItemAdded event, ReducersSignaturesOrder current) { if (current == null) return null; return new ReducersSignaturesOrder( current.orderId(), current.total() + event.amount(), current.closedAt()); }
// (event, current, context) - context carries the event's metadata ReducersSignaturesOrder closed( ReducersSignaturesClosed event, ReducersSignaturesOrder current, EventContext context) { if (current == null) return null; return new ReducersSignaturesOrder( current.orderId(), current.total(), context.getOccurred().toString()); }}defmodule ReducersSyncSignaturesMetricRecorded do use Chronicle.Events.EventType, id: "reducers-sync-signatures-metric-recorded"
defstruct [:value]end
defmodule ReducersSyncSignaturesStats do defstruct total: 0end
defmodule ReducersSyncSignaturesReducer do use Chronicle.Reducers.Reducer, model: ReducersSyncSignaturesStats
alias ReducersSyncSignaturesMetricRecorded
@handles ReducersSyncSignaturesMetricRecorded
# Every reducer has this one shape: the event, the current state, then the # context map — there is no family of overloads to choose between. @impl true def reduce(%ReducersSyncSignaturesMetricRecorded{} = event, current, _context) do total = if current, do: current.total, else: 0 %ReducersSyncSignaturesStats{total: total + event.value} endendimport { EventContext, eventType, reducer } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class ReducersSyncSignaturesOrderPlaced { @field(String) readonly orderId: string;
constructor(orderId: string) { this.orderId = orderId; }}
class ReducersSyncSignaturesOrderSummary { orderId = ''; lastUpdated = new Date();}
@reducer('', undefined, ReducersSyncSignaturesOrderSummary)class ReducersSyncSignaturesOrderSummaryReducer { // Synchronous, with context reducersSyncSignaturesOrderPlaced( event: ReducersSyncSignaturesOrderPlaced, current: ReducersSyncSignaturesOrderSummary | undefined, context: EventContext ): ReducersSyncSignaturesOrderSummary { return { orderId: event.orderId, lastUpdated: context.occurred }; }}Asynchronous Methods
Section titled “Asynchronous Methods”using Cratis.Chronicle.Events;
public interface IReducersGettingStartedAsyncSignatures<TReadModel, TEvent> where TReadModel : class{ // Without context Task<TReadModel?> WithoutContext(TEvent @event, TReadModel? current);
// With context Task<TReadModel?> WithContext(TEvent @event, TReadModel? current, EventContext context);}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModelimport kotlinx.coroutines.delay
@EventTypedata class ReducersAsyncSignaturesOpened(val orderId: String)
@EventTypedata class ReducersAsyncSignaturesItemAdded(val sku: String, val amount: Double)
@ReadModeldata class ReducersAsyncSignaturesOrder( val orderId: String = "", val total: Double = 0.0, val currency: String = "")
class ReducersAsyncSignaturesRates { // Stands in for whatever you actually await - an HTTP client, a cache, a database. suspend fun currencyFor(sku: String): String { delay(1) return if (sku.startsWith("EU")) "EUR" else "USD" }}
@Reducerclass ReducersAsyncSignaturesOrderReducer( private val rates: ReducersAsyncSignaturesRates = ReducersAsyncSignaturesRates()) { // A handler that needs nothing awaited stays an ordinary function. fun opened(event: ReducersAsyncSignaturesOpened) = ReducersAsyncSignaturesOrder(orderId = event.orderId)
// Mark a handler `suspend` and it is awaited rather than blocking the // thread the observation runs on. Every shape - (event), (event, current), // (event, current, context) - may suspend. suspend fun itemAdded( event: ReducersAsyncSignaturesItemAdded, current: ReducersAsyncSignaturesOrder? ) = current?.copy( total = current.total + event.amount, currency = rates.currencyFor(event.sku) )}import io.cratis.chronicle.events.EventContext;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord ReducersAsyncSignaturesOpened(String orderId) {}
@EventTyperecord ReducersAsyncSignaturesItemAdded(String sku, double amount) {}
@ReadModelrecord ReducersAsyncSignaturesOrder(String orderId, double total, String currency) {}
class ReducersAsyncSignaturesRates { // Stands in for whatever you actually call - an HTTP client, a cache, a database. String currencyFor(String sku) { return sku.startsWith("EU") ? "EUR" : "USD"; }}
@Reducerclass ReducersAsyncSignaturesOrderReducer { private final ReducersAsyncSignaturesRates rates = new ReducersAsyncSignaturesRates();
// Every shape Kotlin has is available: (event), (event, current) and // (event, current, context). ReducersAsyncSignaturesOrder opened(ReducersAsyncSignaturesOpened event) { return new ReducersAsyncSignaturesOrder(event.orderId(), 0, ""); }
// Java has no `suspend`, so a handler that calls out to something blocks the thread the // observation runs on. Keep that work short, or hand it to your own executor and await the // result here - the observation only moves on once the handler returns. ReducersAsyncSignaturesOrder itemAdded( ReducersAsyncSignaturesItemAdded event, ReducersAsyncSignaturesOrder current, EventContext context) { ReducersAsyncSignaturesOrder order = current != null ? current : new ReducersAsyncSignaturesOrder(context.getEventSourceId(), 0, "");
return new ReducersAsyncSignaturesOrder( order.orderId(), order.total() + event.amount(), rates.currencyFor(event.sku())); }}Elixir does not support this workflow yet.import { EventContext, eventType, reducer } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class ReducersAsyncSignaturesOrderPlaced { @field(String) readonly orderId: string;
constructor(orderId: string) { this.orderId = orderId; }}
class ReducersAsyncSignaturesOrderSummary { orderId = '';}
@reducer('', undefined, ReducersAsyncSignaturesOrderSummary)class ReducersAsyncSignaturesOrderSummaryReducer { // Async without context async reducersAsyncSignaturesOrderPlaced( event: ReducersAsyncSignaturesOrderPlaced, current: ReducersAsyncSignaturesOrderSummary | undefined ): Promise<ReducersAsyncSignaturesOrderSummary> { return { orderId: event.orderId }; }}These overload tables describe .NET instance-method dispatch. Kotlin and Java match the first
parameter’s event type. TypeScript selects a method by the camelCase event-class name and passes
(event, current, context); it awaits the result, so an async handler works. Kotlin awaits suspend
handlers. Elixir uses a reduce/3 callback pattern-matched by event struct.
Key Points:
- In .NET, method names are unrestricted; the examples use
Onfollowed by the event type name - The
@eventparameter is the specific event being processed - The
currentparameter contains the existing state (null if no previous state exists) - The
EventContextparameter provides metadata like event source ID, occurred timestamp, and sequence number - Both synchronous and asynchronous methods are supported
.NET dispatch and diagnostics
Section titled “.NET dispatch and diagnostics”In .NET, the event must be the first parameter and the nullable current read model the second. An
optional third parameter must be EventContext. Return the model or Task<TReadModel>, including a
nullable result. Returning null deletes or leaves the instance absent. Classic plain Task is also
accepted, but yields null state; a mutation-only Task handler does not retain current state. A method
taking the current model first and object second is not dispatched.
The .NET analyzers report unsupported signatures as CHR0006 and event parameters without [EventType] as CHR0007, including object event parameters. If none of a reducer’s methods can be dispatched, the client skips its kernel registration and logs a warning naming the reducer and explaining why its public reducer-like methods were skipped. The application still starts; correct the signatures so the reducer can observe events.
3. Using the Reducer Attribute (Optional)
Section titled “3. Using the Reducer Attribute (Optional)”You can customize the reducer using the [Reducer] attribute:
using Cratis.Chronicle.Reducers;
public record ReducersGettingStartedAttributeOrderSummary(Guid OrderId);
[Reducer(id: "order-summary", eventSequence: "order-events")]public class ReducersGettingStartedAttributeOrderSummaryReducer : IReducerFor<ReducersGettingStartedAttributeOrderSummary>;import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModel
@ReadModeldata class ReducersGettingStartedAttributeOrderSummary(val orderId: String = "")
@Reducer(id = "order-summary", eventSequence = "outbox", isActive = false)class ReducersGettingStartedAttributeOrderSummaryReducer { fun on(event: ReducersGettingStartedAttributeOrderPlaced) = ReducersGettingStartedAttributeOrderSummary(event.orderId)}
@EventTypedata class ReducersGettingStartedAttributeOrderPlaced(val orderId: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;
@ReadModelrecord ReducersGettingStartedAttributeOrderSummary(String orderId) { ReducersGettingStartedAttributeOrderSummary() { this(""); }}
@Reducer(id = "order-summary", eventSequence = "outbox", isActive = false)class ReducersGettingStartedAttributeOrderSummaryReducer { public ReducersGettingStartedAttributeOrderSummary on(ReducersGettingStartedAttributeOrderPlaced event) { return new ReducersGettingStartedAttributeOrderSummary(event.orderId()); }}
@EventTyperecord ReducersGettingStartedAttributeOrderPlaced(String orderId) {}defmodule MyApp.ReadModels.ReducersGettingStartedAttributeOrderSummary do defstruct order_id: ""end
defmodule MyApp.Reducers.ReducersGettingStartedAttributeOrderSummaryReducer do use Chronicle.Reducers.Reducer, model: MyApp.ReadModels.ReducersGettingStartedAttributeOrderSummary, id: "order-summary"
@impl true def reduce(_event, model, _context), do: modelendimport { reducer } from '@cratis/chronicle';
class ReducersGettingStartedAttributeOrderSummary { orderId = '';}
@reducer('order-summary', 'order-events', ReducersGettingStartedAttributeOrderSummary)class ReducersGettingStartedAttributeOrderSummaryReducer {}Attribute parameters:
id- Custom identifier for the reducer (defaults to the fully qualified type name)eventSequence- The event sequence to observe (defaults to the event log)isActive- Whether the reducer actively observes events (defaults totrue)
Use the client tab’s configuration shape. TypeScript expresses identity and sequence through the
reducer() decorator; Kotlin and Java use @Reducer.
Retrieving Reduced State
Section titled “Retrieving Reduced State”Once your reducer is set up, you can retrieve the computed state:
using Cratis.Chronicle;using Cratis.Chronicle.ReadModels;
public class ReducersGettingStartedOrderService(IEventStore eventStore){ public async Task<ReducersGettingStartedOrderSummary?> GetOrderSummary(Guid orderId) => await eventStore.ReadModels.GetInstanceById<ReducersGettingStartedOrderSummary>(orderId);}import io.cratis.chronicle.IEventStore
class ReducersGettingStartedOrderService(private val store: IEventStore) { suspend fun getOrderSummary(orderId: String): ReducersGettingStartedOrderSummary? = store.readModels.getInstanceByKey(ReducersGettingStartedOrderSummary::class, orderId)}import io.cratis.chronicle.IEventStore;import io.cratis.chronicle.java.BlockingEventStore;
class ReducersGettingStartedOrderService { private final BlockingEventStore store;
ReducersGettingStartedOrderService(IEventStore store) { this.store = new BlockingEventStore(store); }
ReducersGettingStartedOrderSummary getOrderSummary(String orderId) { return store.getReadModels() .getInstanceByKey(ReducersGettingStartedOrderSummary.class, orderId); }}defmodule MyApp.ReducersGettingStartedOrderService do alias MyApp.ReadModels.ReducersGettingStartedOrderSummary
def get_order_summary(order_id) do Chronicle.read_model(ReducersGettingStartedOrderSummary, order_id) endendimport { IEventStore } from '@cratis/chronicle';
class ReducersGettingStartedOrderService { constructor(private readonly store: IEventStore) {}
async getOrderSummary(orderId: string): Promise<ReducersGettingStartedOrderSummary | null> { return this.store.readModels.findInstanceById(ReducersGettingStartedOrderSummary, orderId); }}Event Processing
Section titled “Event Processing”Reducer methods are called for each event matching the event type:
- Event Type Matching - Chronicle calls the method that handles the specific event type
- Event Source ID - Each method receives events for a single event source
- Sequential Processing - Events are processed in the order they were appended
The current parameter in each method:
- Is
nullwhen no previous state exists for this event source - Contains the current persisted state for this event source
Best Practices
Section titled “Best Practices”- Keep reducers deterministic - Compute state from recorded facts. An injected clock or live rate service can return different values during replay; record time and rates in events when they determine the result.
- Handle null current state - Always check if
currentis null for the initial state - Use immutable state - Create new instances rather than mutating the current state
- Process events in order - The events are provided in sequence order; respect that order
- Consider performance - For large event streams, optimize your reduction logic
Next Steps
Section titled “Next Steps”- Learn about Passive Reducers to control when reducers observe
- Explore Event Processing for advanced patterns