Passive Reducers
Passive reducers are registered with the kernel but do not actively observe and materialize events through the sink. This is useful when you want to compute read model state on-demand rather than maintaining it continuously.
Understanding Passive vs Active Reducers
Section titled “Understanding Passive vs Active Reducers”Active Reducers (Default)
Section titled “Active Reducers (Default)”Active reducers:
- Automatically observe new events as they are appended
- Continuously maintain up-to-date read model state
- Persist state through the configured sink (typically MongoDB)
- Are immediately available for querying without computation delay
What Makes a Reducer Passive?
Section titled “What Makes a Reducer Passive?”Passive reducers:
- Are registered but do not actively observe events
- Compute state on-demand when requested
- Do not persist state through the sink
- Are ideal for ad-hoc queries or temporary computations
Making a Reducer Passive
Section titled “Making a Reducer Passive”There are two ways to make a reducer passive:
Option 1: Using the Reducer Attribute
Section titled “Option 1: Using the Reducer Attribute”Set the isActive parameter to false on the reducer class:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Reducers;
[EventType]public record PassiveReducersDataRecorded(decimal Value);
public record PassiveReducersAnalytics(int RecordCount, decimal TotalValue, DateTimeOffset LastUpdated);
[Reducer(isActive: false)]public class PassiveReducersTemporaryAnalyticsReducer : IReducerFor<PassiveReducersAnalytics>{ public PassiveReducersAnalytics Recorded(PassiveReducersDataRecorded @event, PassiveReducersAnalytics? current, EventContext context) { var count = current?.RecordCount ?? 0; var sum = current?.TotalValue ?? 0m;
return new PassiveReducersAnalytics(count + 1, sum + @event.Value, context.Occurred); }}Option 2: Using the Passive Attribute on the Read Model
Section titled “Option 2: Using the Passive Attribute on the Read Model”Mark the read model class with the [Passive] attribute:
using Cratis.Chronicle.Events;using Cratis.Chronicle.ReadModels;using Cratis.Chronicle.Reducers;
[EventType]public record PassiveReducersTransactionCompleted(decimal Amount);
[Passive]public record PassiveReducersAdHocReport( decimal TotalRevenue, int TransactionCount, DateTimeOffset GeneratedAt);
public class PassiveReducersAdHocReportReducer : IReducerFor<PassiveReducersAdHocReport>{ public PassiveReducersAdHocReport Completed(PassiveReducersTransactionCompleted @event, PassiveReducersAdHocReport? current, EventContext context) { var revenue = current?.TotalRevenue ?? 0m; var count = current?.TransactionCount ?? 0;
return new PassiveReducersAdHocReport( revenue + @event.Amount, count + 1, context.Occurred); }}Note: The [Passive] attribute on the read model automatically makes all reducers targeting that read model passive, regardless of individual reducer settings.
Attribute Priority
Section titled “Attribute Priority”When determining if a reducer is passive:
- If the read model has
[Passive]attribute → Reducer is passive - If the reducer has
[Reducer(isActive: false)]→ Reducer is passive - Otherwise → Reducer is active (default)
This means the [Passive] attribute on the read model takes precedence over the reducer’s isActive setting.
Use Cases for Passive Reducers
Section titled “Use Cases for Passive Reducers”1. On-Demand Reports
Section titled “1. On-Demand Reports”Generate reports only when requested, avoiding the overhead of continuous state maintenance:
using Cratis.Chronicle.Events;using Cratis.Chronicle.ReadModels;using Cratis.Chronicle.Reducers;
[EventType]public record PassiveReducersPaymentReceived(string Category, decimal Amount);
[Passive]public record PassiveReducersMonthlyRevenueReport( decimal TotalRevenue, Dictionary<string, decimal> RevenueByCategory, int Month, int Year);
public class PassiveReducersMonthlyRevenueReportReducer : IReducerFor<PassiveReducersMonthlyRevenueReport>{ public PassiveReducersMonthlyRevenueReport Received(PassiveReducersPaymentReceived @event, PassiveReducersMonthlyRevenueReport? current, EventContext context) { var revenue = current?.TotalRevenue ?? 0m; var byCategory = current?.RevenueByCategory ?? new Dictionary<string, decimal>();
if (!byCategory.ContainsKey(@event.Category)) byCategory[@event.Category] = 0;
byCategory[@event.Category] += @event.Amount;
return new PassiveReducersMonthlyRevenueReport( revenue + @event.Amount, byCategory, context.Occurred.Month, context.Occurred.Year); }}2. Temporary Analysis
Section titled “2. Temporary Analysis”Perform exploratory data analysis without polluting your persistent storage:
using Cratis.Chronicle.ReadModels;
[Passive]public record PassiveReducersCustomerBehaviorAnalysis( int UniqueCustomers, decimal AverageOrderValue, Dictionary<int, int> OrdersByHour);3. Historical Snapshots
Section titled “3. Historical Snapshots”Compute read model state at specific points in time without maintaining continuous state:
using Cratis.Chronicle;using Cratis.Chronicle.ReadModels;
public record PassiveReducersAccountBalance(decimal Balance);
public class PassiveReducersHistoricalBalanceService(IEventStore eventStore){ public async Task<PassiveReducersAccountBalance> GetBalanceAtDate(Guid accountId, DateTimeOffset date) { // Passive reducer computes state on-demand from historical events return await eventStore.ReadModels.GetInstanceById<PassiveReducersAccountBalance>(accountId); }}4. Development and Testing
Section titled “4. Development and Testing”During development, you might want to register reducers without activating them:
using Cratis.Chronicle.Reducers;
public record PassiveReducersExperimentalMetrics(int SampleCount);
#if DEBUG[Reducer(isActive: false)]#endifpublic class PassiveReducersExperimentalMetricsReducer : IReducerFor<PassiveReducersExperimentalMetrics>;Retrieving State from Passive Reducers
Section titled “Retrieving State from Passive Reducers”Passive reducers compute state on-demand using the same API as active reducers:
using Cratis.Chronicle;using Cratis.Chronicle.ReadModels;
public class PassiveReducersReportingService(IEventStore eventStore){ public async Task<PassiveReducersMonthlyRevenueReport> GenerateReport(Guid reportId) => // This triggers the passive reducer to compute state from events await eventStore.ReadModels.GetInstanceById<PassiveReducersMonthlyRevenueReport>(reportId);}Performance Considerations
Section titled “Performance Considerations”Trade-offs
Section titled “Trade-offs”Active Reducers:
- ✅ Fast queries (state is pre-computed)
- ✅ Always up-to-date
- ❌ Continuous resource usage
- ❌ Storage overhead
Passive Reducers:
- ✅ No continuous resource usage
- ✅ No storage overhead
- ❌ Slower queries (computed on-demand)
- ❌ Not pre-computed
Best Practices
Section titled “Best Practices”- Use active reducers for frequently accessed data - When read model state is queried often
- Use passive reducers for infrequent queries - When data is requested occasionally
- Consider caching - For passive reducers with expensive computations
- Monitor performance - Track computation time for passive reducers
- Event volume matters - Passive reducers process all events each time; large event streams may be slow
Switching Between Active and Passive
Section titled “Switching Between Active and Passive”You can change a reducer from active to passive (or vice versa) by updating the attribute:
using Cratis.Chronicle.Reducers;
public record PassiveReducersSwitchableReadModel(int Value);
// Was active, now passive[Reducer(isActive: false)]public class PassiveReducersSwitchableReducer : IReducerFor<PassiveReducersSwitchableReadModel>;When you change the isActive setting:
- The reducer definition is updated in the kernel
- Active reducers will unsubscribe from the event stream
- Passive reducers will not start observing
- Persisted state (for previously active reducers) remains in storage but won’t be updated
Summary
Section titled “Summary”Passive reducers provide flexibility in how you manage read model state:
- Use
[Reducer(isActive: false)]to make a specific reducer passive - Use
[Passive]on the read model to make all reducers for that model passive - The
[Passive]attribute takes precedence over individual reducer settings - Passive reducers are ideal for on-demand computations, reports, and analysis
- Choose active vs. passive based on access frequency and resource constraints