CHR0036: Reducer must not have mutable state
Rule Description
Section titled “Rule Description”A class implementing IReducerFor<T> declares mutable instance state — a non-readonly field or a settable (non-init) property — or injects a storage primitive such as IMongoCollection<T> through its constructor. Reducers fold events into read-model state and Chronicle re-creates and replays them, so they must be stateless and deterministic.
Keep the reducer stateless: express dependencies as readonly, primary-constructor-injected fields, read keyed state through an injected read model or IReadModels.GetInstanceById, and derive everything else from the current state and the event.
Severity
Section titled “Severity”Warning
Example
Section titled “Example”using Cratis.Chronicle.Events;using Cratis.Chronicle.Reducers;
// Warning CHR0036: Reducer 'Chr0036BalanceReducer' declares mutable state '_runningTotal'.// Reducers must be stateless for deterministic replay.public class Chr0036BalanceReducer : IReducerFor<Chr0036Balance>{ decimal _runningTotal;
public Chr0036Balance Reduce(Chr0036AmountDeposited @event, Chr0036Balance? current) { _runningTotal += @event.Amount; return new(_runningTotal); }}
[EventType]public record Chr0036AmountDeposited(decimal Amount);
public record Chr0036Balance(decimal Total);Why This Rule Exists
Section titled “Why This Rule Exists”A reducer is a pure function of (current state, event) → next state. Chronicle drives that function by replaying the event stream — during initial projection, after a rewind, on recovery, or when rebuilding a read model — and a single event source may be folded more than once.
Mutable instance state breaks that contract. A running total accumulated in a field is wrong the moment the stream replays, and a value can bleed from one event source into the next when the instance is reused. Everything the next state depends on is already available: the current state parameter (the fold’s accumulator, which Chronicle persists between events) and the event being handled. Anything derived should be computed from those inputs each time, not stored on the instance.
Injecting a storage primitive like IMongoCollection<T> is the same failure from the other side: it lets the reducer read or write the sink directly, outside the deterministic fold, coupling it to where state is persisted. Read state through the read-model abstraction instead.
This rule mirrors CHR0031 and CHR0032 for reactors — reducers carry the identical statelessness requirement.
Related Rules
Section titled “Related Rules”- CHR0031: Reactor must not have mutable state — the same requirement for reactors.
- CHR0032: Reactor must not access storage directly — read state through a read model, not a sink.