Skip to content

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.

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

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

There are two ways to make a reducer passive:

Set the isActive parameter to false on the reducer class:

[Reducer(isActive: false)]
public class TemporaryAnalyticsReducer : IReducerFor<Analytics>
{
public Analytics OnDataRecorded(DataRecorded @event, Analytics? current, EventContext context)
{
var count = current?.RecordCount ?? 0;
var sum = current?.TotalValue ?? 0m;
return new Analytics(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.ReadModels;
[Passive]
public record AdHocReport(
decimal TotalRevenue,
int TransactionCount,
DateTimeOffset GeneratedAt);
public class AdHocReportReducer : IReducerFor<AdHocReport>
{
public AdHocReport OnTransactionCompleted(TransactionCompleted @event, AdHocReport? current, EventContext context)
{
var revenue = current?.TotalRevenue ?? 0m;
var count = current?.TransactionCount ?? 0;
return new AdHocReport(
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.

When determining if a reducer is passive:

  1. If the read model has [Passive] attribute → Reducer is passive
  2. If the reducer has [Reducer(isActive: false)]Reducer is passive
  3. Otherwise → Reducer is active (default)

This means the [Passive] attribute on the read model takes precedence over the reducer’s isActive setting.

Generate reports only when requested, avoiding the overhead of continuous state maintenance:

[Passive]
public record MonthlyRevenueReport(
decimal TotalRevenue,
Dictionary<string, decimal> RevenueByCategory,
int Month,
int Year);
public class MonthlyRevenueReportReducer : IReducerFor<MonthlyRevenueReport>
{
public MonthlyRevenueReport OnPaymentReceived(PaymentReceived @event, MonthlyRevenueReport? 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 MonthlyRevenueReport(
revenue + @event.Amount,
byCategory,
context.Occurred.Month,
context.Occurred.Year);
}
}

Perform exploratory data analysis without polluting your persistent storage:

[Passive]
public record CustomerBehaviorAnalysis(
int UniqueCustomers,
decimal AverageOrderValue,
Dictionary<int, int> OrdersByHour);

Compute read model state at specific points in time without maintaining continuous state:

public class HistoricalBalanceService
{
readonly IEventStore _eventStore;
public async Task<AccountBalance> GetBalanceAtDate(Guid accountId, DateTimeOffset date)
{
// Passive reducer computes state on-demand from historical events
return await _eventStore.ReadModels.GetInstanceById<AccountBalance>(accountId);
}
}

During development, you might want to register reducers without activating them:

#if DEBUG
[Reducer(isActive: false)]
#endif
public class ExperimentalMetricsReducer : IReducerFor<ExperimentalMetrics>
{
// Development/testing reducer
}

Passive reducers compute state on-demand using the same API as active reducers:

public class ReportingService
{
readonly IEventStore _eventStore;
public ReportingService(IEventStore eventStore)
{
_eventStore = eventStore;
}
public async Task<MonthlyRevenueReport> GenerateReport(Guid reportId)
{
// This triggers the passive reducer to compute state from events
return await _eventStore.ReadModels.GetInstanceById<MonthlyRevenueReport>(reportId);
}
}

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
  1. Use active reducers for frequently accessed data - When read model state is queried often
  2. Use passive reducers for infrequent queries - When data is requested occasionally
  3. Consider caching - For passive reducers with expensive computations
  4. Monitor performance - Track computation time for passive reducers
  5. Event volume matters - Passive reducers process all events each time; large event streams may be slow

You can change a reducer from active to passive (or vice versa) by updating the attribute:

// Was active, now passive
[Reducer(isActive: false)]
public class MyReducer : IReducerFor<MyReadModel>
{
// Implementation
}

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

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