Behaviour and macro for defining Chronicle reducers.
Reducers build and maintain read models by folding events into state. Each event type that the reducer handles is applied in order, progressively updating the read model from its initial state.
Defining a reducer
Use Chronicle.Reducers.Reducer in a module, declare which events it handles with
@handles, and implement the reduce/3 callback.
defmodule MyApp.Reducers.AccountReducer do
use Chronicle.Reducers.Reducer, model: MyApp.ReadModels.Account
@handles MyApp.Events.AccountOpened
@handles MyApp.Events.FundsDeposited
@handles MyApp.Events.FundsWithdrawn
@impl true
def reduce(%MyApp.Events.AccountOpened{} = event, _model, _context) do
%MyApp.ReadModels.Account{
account_id: event.account_id,
owner_name: event.owner_name,
balance: event.initial_balance
}
end
def reduce(%MyApp.Events.FundsDeposited{} = event, model, _context) do
%{model | balance: model.balance + event.amount}
end
def reduce(%MyApp.Events.FundsWithdrawn{} = event, model, _context) do
%{model | balance: model.balance - event.amount}
end
endOptions for use Chronicle.Reducers.Reducer
:model— (required) the read model module this reducer produces.:id— a stable string identifier. Defaults to the module's full name.:active— whether Chronicle actively keeps this reducer's read model up to date as events are appended. Defaults totrue. Set tofalsefor a passive reducer: its read model is only computed on demand (e.g. viaChronicle.ReadModels.get/3) and is not kept warm in the background — useful for a reducer whose only purpose is a command-side decision that's read rarely, where continuous observation would be wasted work.
Registering with Chronicle.Client
{Chronicle.Client,
...
reducers: [MyApp.Reducers.AccountReducer]}How reducers work
When Chronicle needs the current state of a read model, it:
- Fetches the stored read model JSON (or starts with
nil) - Fetches all new events for the relevant event source
- Sends them to the reducer as a
ReduceOperationMessage - The reducer applies each event via
reduce/3and returns the final state
The model is stored by Chronicle between calls, so reduce/3 only receives
events that occurred since the last successful reduction.
Event context
The third argument to reduce/3 is a map with:
:event_source_id— the event source (e.g. aggregate ID):sequence_number— the event's position in the event log:occurred— when the event was appended (ISO 8601 string):observation_state— the observation state (:initial,:replay, etc.)
Replay lifecycle (optional)
Implement any of on_replay_begin/0, on_replay_end/0,
on_partition_replay_begin/1, on_partition_replay_end/1 to be notified when
Chronicle starts or finishes replaying this reducer — either as a whole, or
for a single partition (event source). All four are optional; implement only
the ones you need. These are notifications, not events to reduce — they run
outside the normal reduce/3 dispatch, and a raised exception is logged and
swallowed rather than reported to Chronicle.
Summary
Callbacks
Called when replay of a single partition (event source) begins.
Called when replay of a single partition (event source) ends.
Called when a full replay of this reducer begins.
Called when a full replay of this reducer ends.
Applies an event to the current read model state.
Callbacks
Called when replay of a single partition (event source) begins.
Called when replay of a single partition (event source) ends.
@callback on_replay_begin() :: any()
Called when a full replay of this reducer begins.
@callback on_replay_end() :: any()
Called when a full replay of this reducer ends.
Applies an event to the current read model state.
Receives the event struct, the current model (or nil on first event), and
a context map. Returns the updated read model struct.