Skip to content

Projection with NotRewindable

The NotRewindable() method marks a projection as forward-only, meaning it cannot be replayed or rewound. This is useful for projections that maintain state that should not be recreated from scratch or for performance-critical scenarios where replay is not needed.

Use NotRewindable() to mark a projection as forward-only:

using Cratis.Chronicle.Projections;
public class DecNotRewindableAuditLogProjection : IProjectionFor<DecNotRewindableAuditLogEntry>
{
public void Define(IProjectionBuilderFor<DecNotRewindableAuditLogEntry> builder) => builder
.NotRewindable()
.AutoMap()
.FromEvery(_ => _
.Set(m => m.ProcessedAt).ToEventContextProperty(c => c.Occurred))
.From<DecNotRewindableUserAction>(_ => _
.Set(m => m.OccurredAt).ToEventContextProperty(c => c.Occurred));
}

This projection:

  • Cannot be replayed or reset
  • Processes events only as they arrive in real-time
  • Maintains forward-only state progression
  • Is optimized for performance and append-only scenarios

The read model can include timestamp and audit information:

public record DecNotRewindableAuditLogEntry(
string UserId,
string Action,
string Details,
DateTimeOffset OccurredAt,
DateTimeOffset ProcessedAt,
long SequenceNumber);

Events should be designed for forward-only processing:

using Cratis.Chronicle.Events;
[EventType]
public record DecNotRewindableUserAction(
string UserId,
string ActionType,
string Details);
[EventType]
public record DecNotRewindableSystemEvent(
string ComponentName,
string EventType,
string Data);

When a projection is marked as NotRewindable():

  1. No replay capability: The projection cannot be reset and rebuilt from the beginning
  2. Forward-only processing: Events are processed only as they arrive in chronological order
  3. Performance optimization: Chronicle skips replay infrastructure and optimizations
  4. State preservation: Existing projection state is preserved and cannot be recreated
  5. Error handling: Failed events may require manual intervention since replay isn’t available

Perfect for audit logs where you want to preserve the exact timing and sequence:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record DecNotRewindableUserLoginAttempt(string UserId, bool Succeeded);
[EventType]
public record DecNotRewindablePermissionChange(string UserId, string Permission);
public record DecNotRewindableSecurityAuditEntry(
DateTimeOffset AuditedAt,
long SequenceNumber);
public class DecNotRewindableSecurityAuditProjection : IProjectionFor<DecNotRewindableSecurityAuditEntry>
{
public void Define(IProjectionBuilderFor<DecNotRewindableSecurityAuditEntry> builder) => builder
.NotRewindable()
.AutoMap()
.FromEvery(_ => _
.Set(m => m.AuditedAt).ToEventContextProperty(c => c.Occurred)
.Set(m => m.SequenceNumber).ToEventContextProperty(c => c.SequenceNumber))
.From<DecNotRewindableUserLoginAttempt>()
.From<DecNotRewindablePermissionChange>();
}

For real-time performance metrics that don’t need historical accuracy:

[EventType]
public record DecNotRewindableApiRequestCompleted(string Endpoint, int StatusCode, long DurationMilliseconds);
public record DecNotRewindablePerformanceMetric(DateTimeOffset Timestamp);
public class DecNotRewindablePerformanceMetricProjection : IProjectionFor<DecNotRewindablePerformanceMetric>
{
public void Define(IProjectionBuilderFor<DecNotRewindablePerformanceMetric> builder) => builder
.NotRewindable()
.AutoMap()
.From<DecNotRewindableApiRequestCompleted>(_ => _
.Set(m => m.Timestamp).ToEventContextProperty(c => c.Occurred));
}

For append-only financial records where replay could cause confusion:

[EventType]
public record DecNotRewindablePaymentProcessed(string PaymentId, decimal Amount);
public record DecNotRewindableLedgerEntry(
DateTimeOffset RecordedAt,
string TransactionType);
public class DecNotRewindableTransactionLedgerProjection : IProjectionFor<DecNotRewindableLedgerEntry>
{
public void Define(IProjectionBuilderFor<DecNotRewindableLedgerEntry> builder) => builder
.NotRewindable()
.AutoMap()
.FromEvery(_ => _
.Set(m => m.RecordedAt).ToEventContextProperty(c => c.Occurred))
.From<DecNotRewindablePaymentProcessed>(_ => _
.Set(m => m.TransactionType).ToValue("PAYMENT"));
}

Use NotRewindable() when:

  • Audit requirements: You need to preserve exact event timestamps and sequence
  • Performance critical: The projection handles high-volume events and replay would be expensive
  • Append-only data: The projection represents data that should never be regenerated
  • Real-time processing: Only forward, real-time event flow matters — a rebuilt-from-replay history would not reflect current state
  • External integrations: The projection triggers external actions that shouldn’t be repeated
  • Compliance: Regulatory requirements prevent data recreation or replay

Avoid NotRewindable() when:

  • Business logic changes: You might need to replay events with updated logic
  • Bug fixes: Errors in projection logic need to be corrected by replay
  • Data migration: You need to rebuild projections with new schemas
  • Testing: Development and testing scenarios benefit from replay capability
  • Recovery: System failures might require rebuilding projection state

NotRewindable can be combined with other projection features:

using Cratis.Chronicle.Projections;
[EventType]
public record DecNotRewindableOrderReceived(string OrderId);
[EventType]
public record DecNotRewindableOrderProcessing(string OrderId);
[EventType]
public record DecNotRewindableOrderCompleted(string OrderId);
public record DecNotRewindableOrderStatus(
string Status,
DateTimeOffset LastUpdatedAt);
public class DecNotRewindableRealTimeOrderStatusProjection : IProjectionFor<DecNotRewindableOrderStatus>
{
public void Define(IProjectionBuilderFor<DecNotRewindableOrderStatus> builder) => builder
.NotRewindable()
.FromEventSequence("order-processing")
.Passive()
.AutoMap()
.FromEvery(_ => _
.Set(m => m.LastUpdatedAt).ToEventContextProperty(c => c.Occurred))
.From<DecNotRewindableOrderReceived>(_ => _
.Set(m => m.Status).ToValue("RECEIVED"))
.From<DecNotRewindableOrderProcessing>(_ => _
.Set(m => m.Status).ToValue("PROCESSING"))
.From<DecNotRewindableOrderCompleted>(_ => _
.Set(m => m.Status).ToValue("COMPLETED"));
}

This combines non-rewindable behavior with event sequence specification, passive mode, and event-context timestamp mapping.

With non-rewindable projections:

  • Failed events: Must be handled carefully since replay isn’t available
  • Dead letter queues: Consider implementing for failed event processing
  • Manual intervention: May be required to fix projection state
  • Monitoring: Implement comprehensive monitoring and alerting
  • Backup strategies: Consider point-in-time snapshots for recovery

Non-rewindable projections offer several performance advantages:

  • Reduced memory usage: No need to store replay state or checkpoints
  • Faster startup: No replay phase during application startup
  • Lower CPU usage: Eliminates replay processing overhead
  • Simplified infrastructure: Fewer moving parts in the projection system
  • Better throughput: All processing power focused on real-time events