Reducers
Reducers provide a way to build read models by reducing a sequence of events into a single state. Unlike projections, which focus on transforming individual events into read models, reducers process collections of events together to compute derived state.
Key Concepts
Section titled “Key Concepts”A reducer is a specialized observer that:
- Processes multiple events together - Events are grouped by event source and passed to the reducer as a collection
- Computes derived state - The reducer method receives the current state and returns the new state after processing the events
- Maintains temporal consistency - All events for a given event source are processed in order
- Supports snapshots - The computed state can be retrieved at any point in the event stream
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
- Implement complex business logic - Process events together to derive insights that span multiple events
- Create snapshots - Capture the state of a read model at specific points in time
Basic Example
Section titled “Basic Example”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
@EventType(id = "reducers-index-deposit-made")data class ReducersIndexDepositMade(val amount: Double)
@EventType(id = "reducers-index-withdrawal-made")data 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;
@EventType(id = "reducers-index-deposit-made")record ReducersIndexDepositMade(double amount) {}
@EventType(id = "reducers-index-withdrawal-made")record 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() }; }}Topics
Section titled “Topics”- Getting Started - Learn how to create your first reducer
- Choose a read-model style - Compare reducers with model-bound and declarative projections
- 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 with strong consistency
- Getting a Collection of Instances - Retrieve all instances for reporting and analysis
- Getting Snapshots - Retrieve historical state snapshots grouped by correlation ID
- Watching Read Models - Observe real-time changes as events are applied