Reducers
A Chronicle reducer is an observer that folds events into read-model state using code you write.
When a view depends on the previous state, writing its transitions as code can be clearer than mapping properties. A reducer folds events one at a time, receiving the state the previous event produced and returning the next state for that event source.
Start with model-bound projection attributes, use fluent mappings when the definition needs structure, and choose a reducer when state-transition code is clearer; a self-reducing model is the default for new reducer code. Choose how to build a read model compares the forms and shows when a separate instance reducer is useful.
Key Concepts
Section titled “Key Concepts”A reducer is a specialized observer that:
- Processes events one at a time, in order - Chronicle may deliver several events for an event source in one batch, but it calls your reducer method once per event, passing the state the previous event produced
- Computes derived state - The reducer method receives the event and the current state, and returns the new state
- Maintains temporal consistency - All events for a given event source are processed in order
- Maintains materialized state - Read the currently stored state; historical reducer snapshots are not provided by
GetSnapshotsById
When to Use Reducers
Section titled “When to Use Reducers”Reducers are ideal when you need to:
- Aggregate data across multiple events - Calculate sums, averages, or other metrics from a series of events
- Build temporal models - Track how state changes over time
- Express state-dependent logic - Branch on previous state or calculate values with guards
- Reconstruct state - Fold a history on demand or rebuild materialized state during replay
Start with transitions on the model
Section titled “Start with transitions on the model”using Cratis.Chronicle.Events;
[EventType]public record SelfBalanceAdded(decimal Amount);
[EventType]public record SelfBalanceCleared;
public record SelfAccountBalance(decimal Balance){ public static SelfAccountBalance Reduce(SelfBalanceAdded @event, SelfAccountBalance? current) => new((current?.Balance ?? 0m) + @event.Amount);
public static SelfAccountBalance? Reduce(SelfBalanceCleared @event, SelfAccountBalance? current) => null;}Kotlin does not support this workflow yet.Track self-reducing read model support: https://github.com/Cratis/Chronicle/issues/4851Java does not support this workflow yet.Track self-reducing read model support: https://github.com/Cratis/Chronicle/issues/4851This Chronicle client does not support this workflow yet.Track https://github.com/Cratis/Chronicle/issues/4851 before using self-reducing read models from Elixir.TypeScript does not support this workflow yet.Track https://github.com/Cratis/Chronicle/issues/4851 before using this API from this language.The static Reduce shape lets Chronicle discover the model’s fold without a marker or separate class.
Continue with self-reducing read models to register, test, and configure it.
Use IReducerFor<T> when the fold needs injected services, instance behavior during replay, or must
live apart from a shared model contract. See the separate-reducer guide.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Reducers;
[EventType]public record ReducersIndexDepositMade(decimal Amount);
[EventType]public record ReducersIndexWithdrawalMade(decimal Amount);
public record ReducersIndexAccountBalance(decimal Balance, DateTimeOffset LastUpdated);
public class ReducersIndexAccountBalanceReducer : IReducerFor<ReducersIndexAccountBalance>{ public ReducersIndexAccountBalance Deposited(ReducersIndexDepositMade @event, ReducersIndexAccountBalance? current, EventContext context) { var currentBalance = current?.Balance ?? 0m; return new ReducersIndexAccountBalance(currentBalance + @event.Amount, context.Occurred); }
public ReducersIndexAccountBalance WithdrawalMade(ReducersIndexWithdrawalMade @event, ReducersIndexAccountBalance? current, EventContext context) { var currentBalance = current?.Balance ?? 0m; return new ReducersIndexAccountBalance(currentBalance - @event.Amount, context.Occurred); }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModelimport java.time.Instant
@EventTypedata class ReducersIndexDepositMade(val amount: Double)
@EventTypedata class ReducersIndexWithdrawalMade(val amount: Double)
@ReadModeldata class ReducersIndexAccountBalance(val balance: Double = 0.0, val lastUpdated: Instant = Instant.EPOCH)
@Reducerclass ReducersIndexAccountBalanceReducer { fun deposited(event: ReducersIndexDepositMade, current: ReducersIndexAccountBalance?): ReducersIndexAccountBalance { val currentBalance = current?.balance ?: 0.0 return ReducersIndexAccountBalance(currentBalance + event.amount, Instant.now()) }
fun withdrawalMade(event: ReducersIndexWithdrawalMade, current: ReducersIndexAccountBalance?): ReducersIndexAccountBalance { val currentBalance = current?.balance ?: 0.0 return ReducersIndexAccountBalance(currentBalance - event.amount, Instant.now()) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;import java.time.Instant;
@EventTyperecord ReducersIndexDepositMade(double amount) {}
@EventTyperecord ReducersIndexWithdrawalMade(double amount) {}
@ReadModelrecord ReducersIndexAccountBalance(double balance, Instant lastUpdated) { ReducersIndexAccountBalance() { this(0.0, Instant.EPOCH); }}
@Reducerclass ReducersIndexAccountBalanceReducer { ReducersIndexAccountBalance deposited(ReducersIndexDepositMade event, ReducersIndexAccountBalance current) { double currentBalance = current != null ? current.balance() : 0.0; return new ReducersIndexAccountBalance(currentBalance + event.amount(), Instant.now()); }
ReducersIndexAccountBalance withdrawalMade(ReducersIndexWithdrawalMade event, ReducersIndexAccountBalance current) { double currentBalance = current != null ? current.balance() : 0.0; return new ReducersIndexAccountBalance(currentBalance - event.amount(), Instant.now()); }}defmodule MyApp.Events.ReducersIndexDepositMade do use Chronicle.Events.EventType, id: "reducers-index-deposit-made"
defstruct [:amount]end
defmodule MyApp.Events.ReducersIndexWithdrawalMade do use Chronicle.Events.EventType, id: "reducers-index-withdrawal-made"
defstruct [:amount]end
defmodule MyApp.ReadModels.ReducersIndexAccountBalance do defstruct balance: 0, last_updated: nilend
defmodule MyApp.Reducers.ReducersIndexAccountBalanceReducer do use Chronicle.Reducers.Reducer, model: MyApp.ReadModels.ReducersIndexAccountBalance
alias MyApp.Events.{ReducersIndexDepositMade, ReducersIndexWithdrawalMade} alias MyApp.ReadModels.ReducersIndexAccountBalance
@handles ReducersIndexDepositMade @handles ReducersIndexWithdrawalMade
@impl true def reduce(%ReducersIndexDepositMade{} = event, current, context) do current_balance = if current, do: current.balance, else: 0
%ReducersIndexAccountBalance{ balance: current_balance + event.amount, last_updated: Map.get(context, :occurred) } end
def reduce(%ReducersIndexWithdrawalMade{} = event, current, context) do current_balance = if current, do: current.balance, else: 0
%ReducersIndexAccountBalance{ balance: current_balance - event.amount, last_updated: Map.get(context, :occurred) } endendimport { eventType, reducer } from '@cratis/chronicle';
@eventType()class ReducersIndexDepositMade { amount = 0;}
@eventType()class ReducersIndexWithdrawalMade { amount = 0;}
class ReducersIndexAccountBalance { balance = 0; lastUpdated: Date = new Date(0);}
// Handler method names must be the exact camelCase of the event's class name -// Chronicle discovers handlers by name, not by parameter type.@reducer('', undefined, ReducersIndexAccountBalance)class ReducersIndexAccountBalanceReducer { reducersIndexDepositMade( event: ReducersIndexDepositMade, current: ReducersIndexAccountBalance | undefined ): ReducersIndexAccountBalance { const currentBalance = current?.balance ?? 0; return { balance: currentBalance + event.amount, lastUpdated: new Date() }; }
reducersIndexWithdrawalMade( event: ReducersIndexWithdrawalMade, current: ReducersIndexAccountBalance | undefined ): ReducersIndexAccountBalance { const currentBalance = current?.balance ?? 0; return { balance: currentBalance - event.amount, lastUpdated: new Date() }; }}In .NET, a reducer class must target only one read model. Implementing two closed IReducerFor<TReadModel> interfaces, including through inheritance, throws TypeMustImplementOnlyOneReducer during default artifact discovery. Split the class into one reducer per read model. CHR0057 reports this before runtime.
The .NET client can also explicitly nominate a plain classic reducer and infer its single read model from supported instance-method signatures. Alternatively, a concrete model with the exact public static Reduce(event, nullable current[, EventContext]) shape is discovered as a self reducer, without a marker. The existing RegisterReducer APIs accept either form; see explicit artifact registration.
Topics
Section titled “Topics”- Getting Started - Learn how to create your first reducer
- Self-reducing read models - Keep static transitions on the read model
- Choose how to build a read model - One decision path with worked examples and switching costs
- Subscribe to External Event Stores - Configure outbox-to-inbox subscriptions for reducers
- Passive Reducers - Control when reducers actively observe events
- Event Processing - Understand how reducers process events
- Event Sequence - Specify which event sequence a reducer observes
- Filtering by appended event metadata - Limit reducers to specific tags, event source types, or event stream types
- Tagging Reducers - How to use tags with reducers
Reading Your Reducer-Based Read Models
Section titled “Reading Your Reducer-Based Read Models”Once you’ve defined a reducer, you can retrieve and observe the resulting read models using the IReadModels API:
- Getting a Single Instance - Retrieve a specific instance by key
- Getting a Collection of Instances - Retrieve all instances for reporting and analysis
- Getting Snapshots - Historical projection snapshots;
GetSnapshotsByIdcurrently returns no reducer history - Watching Read Models - Observe real-time changes as events are applied