dotnet-cqrs/docs/event-streaming/stream-configuration/lifecycle-config.md

12 KiB

Lifecycle Configuration

Automate stream lifecycle management with automatic creation, archival, and deletion.

Overview

Lifecycle configuration automates stream management:

  • Auto-Create - Create streams on first append
  • Auto-Archive - Move old events to cold storage
  • Auto-Delete - Delete archived or expired events
  • Custom Archive Locations - Specify S3, Azure Blob, or file storage

Quick Start

using Svrnty.CQRS.Events.Abstractions;

var configStore = serviceProvider.GetRequiredService<IStreamConfigurationStore>();

await configStore.SetConfigurationAsync(new StreamConfiguration
{
    StreamName = "orders",
    Lifecycle = new LifecycleConfiguration
    {
        AutoCreate = true,
        AutoArchive = true,
        ArchiveAfter = TimeSpan.FromDays(90),
        ArchiveLocation = "s3://archive/orders"
    }
});

Lifecycle Properties

public class LifecycleConfiguration
{
    public bool AutoCreate { get; set; }                   // Create on first append
    public bool AutoArchive { get; set; }                  // Enable archival
    public TimeSpan ArchiveAfter { get; set; }             // Archive age threshold
    public string? ArchiveLocation { get; set; }           // Archive storage URI
    public bool AutoDelete { get; set; }                   // Delete after archive
    public TimeSpan DeleteAfter { get; set; }              // Delete age threshold
    public bool CompressOnArchive { get; set; }            // Compress archived events
    public string? ArchiveFormat { get; set; }             // Parquet, JSON, Avro
}

Auto-Create

Automatically create streams on first append:

// Enable auto-create (default: false)
var lifecycle = new LifecycleConfiguration
{
    AutoCreate = true
};

await configStore.SetConfigurationAsync(new StreamConfiguration
{
    StreamName = "user-activity",
    Lifecycle = lifecycle
});

// Now you can append without explicitly creating stream
await eventStore.AppendAsync("user-activity", new UserLoginEvent());
// Stream created automatically

Manual vs Auto-Create

// ❌ Manual - Requires explicit creation
await eventStore.CreateStreamAsync("orders");
await eventStore.AppendAsync("orders", @event);

// ✅ Auto-Create - Stream created on first append
var lifecycle = new LifecycleConfiguration { AutoCreate = true };
await eventStore.AppendAsync("orders", @event);  // Creates if not exists

Auto-Archive

Move old events to cold storage:

// Archive after 90 days to S3
var lifecycle = new LifecycleConfiguration
{
    AutoArchive = true,
    ArchiveAfter = TimeSpan.FromDays(90),
    ArchiveLocation = "s3://my-bucket/archives/orders",
    CompressOnArchive = true,
    ArchiveFormat = "parquet"  // Efficient columnar format
};

await configStore.SetConfigurationAsync(new StreamConfiguration
{
    StreamName = "orders",
    Lifecycle = lifecycle
});

Archive Locations

S3

var lifecycle = new LifecycleConfiguration
{
    AutoArchive = true,
    ArchiveAfter = TimeSpan.FromDays(365),
    ArchiveLocation = "s3://prod-archives/orders/{year}/{month}",
    CompressOnArchive = true
};

// Results in: s3://prod-archives/orders/2025/12/events-12345.parquet.gz

Azure Blob Storage

var lifecycle = new LifecycleConfiguration
{
    AutoArchive = true,
    ArchiveAfter = TimeSpan.FromDays(365),
    ArchiveLocation = "azure://archivescontainer/orders/{year}/{month}",
    CompressOnArchive = true
};

Local/Network File System

var lifecycle = new LifecycleConfiguration
{
    AutoArchive = true,
    ArchiveAfter = TimeSpan.FromDays(30),
    ArchiveLocation = "file:///mnt/archives/orders/{year}/{month}",
    CompressOnArchive = true
};

Auto-Delete

Automatically delete old or archived events:

// Delete after archiving
var lifecycle = new LifecycleConfiguration
{
    AutoArchive = true,
    ArchiveAfter = TimeSpan.FromDays(90),
    ArchiveLocation = "s3://archives/orders",
    AutoDelete = true,
    DeleteAfter = TimeSpan.FromDays(100)  // Delete 10 days after archive
};

// Delete without archiving (data loss!)
var lifecycle = new LifecycleConfiguration
{
    AutoArchive = false,
    AutoDelete = true,
    DeleteAfter = TimeSpan.FromDays(7)  // Delete after 7 days
};

Archive Formats

var lifecycle = new LifecycleConfiguration
{
    AutoArchive = true,
    ArchiveAfter = TimeSpan.FromDays(90),
    ArchiveLocation = "s3://archives/orders",
    ArchiveFormat = "parquet",  // Columnar, efficient for analytics
    CompressOnArchive = true
};

// Best for:
// - Analytics queries
// - Large datasets
// - Efficient storage

JSON

var lifecycle = new LifecycleConfiguration
{
    AutoArchive = true,
    ArchiveAfter = TimeSpan.FromDays(90),
    ArchiveLocation = "s3://archives/orders",
    ArchiveFormat = "json",  // Human-readable
    CompressOnArchive = true  // GZIP compression
};

// Best for:
// - Human inspection
// - Simple tooling
// - Debugging

Avro

var lifecycle = new LifecycleConfiguration
{
    AutoArchive = true,
    ArchiveAfter = TimeSpan.FromDays(90),
    ArchiveLocation = "s3://archives/orders",
    ArchiveFormat = "avro",  // Schema evolution support
    CompressOnArchive = true
};

// Best for:
// - Schema evolution
// - Cross-language compatibility
// - Event versioning

Domain-Specific Examples

Audit Logs - Long-term Archival

var auditLifecycle = new LifecycleConfiguration
{
    AutoCreate = true,
    AutoArchive = true,
    ArchiveAfter = TimeSpan.FromDays(365),  // Archive after 1 year
    ArchiveLocation = "s3://compliance-archives/audit-logs/{year}",
    CompressOnArchive = true,
    ArchiveFormat = "parquet",
    AutoDelete = false  // Keep in database AND archive for compliance
};

await configStore.SetConfigurationAsync(new StreamConfiguration
{
    StreamName = "audit-logs",
    Lifecycle = auditLifecycle,
    Tags = new List<string> { "compliance", "audit" }
});

Analytics Events - Archive and Delete

var analyticsLifecycle = new LifecycleConfiguration
{
    AutoCreate = true,
    AutoArchive = true,
    ArchiveAfter = TimeSpan.FromDays(90),  // Archive after 90 days
    ArchiveLocation = "s3://analytics-archives/events/{year}/{month}",
    CompressOnArchive = true,
    ArchiveFormat = "parquet",  // Efficient for analytics
    AutoDelete = true,
    DeleteAfter = TimeSpan.FromDays(100)  // Delete from DB after archive
};

await configStore.SetConfigurationAsync(new StreamConfiguration
{
    StreamName = "analytics",
    Lifecycle = analyticsLifecycle
});

Temporary Sessions - Delete Only

var sessionLifecycle = new LifecycleConfiguration
{
    AutoCreate = true,
    AutoArchive = false,  // No archival needed
    AutoDelete = true,
    DeleteAfter = TimeSpan.FromHours(24)  // Delete after 24 hours
};

await configStore.SetConfigurationAsync(new StreamConfiguration
{
    StreamName = "user-sessions",
    Lifecycle = sessionLifecycle,
    Tags = new List<string> { "temporary" }
});

Financial Transactions - Permanent Archive

var financialLifecycle = new LifecycleConfiguration
{
    AutoCreate = true,
    AutoArchive = true,
    ArchiveAfter = TimeSpan.FromDays(180),  // Archive after 6 months
    ArchiveLocation = "s3://financial-archives/transactions/{year}/{month}",
    CompressOnArchive = true,
    ArchiveFormat = "parquet",
    AutoDelete = false  // Never delete, keep both DB and archive
};

await configStore.SetConfigurationAsync(new StreamConfiguration
{
    StreamName = "financial-transactions",
    Lifecycle = financialLifecycle,
    Tags = new List<string> { "financial", "compliance", "permanent" }
});

Archive Process

Automatic Archival

// Background service handles archival automatically
public class ArchivalBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromHours(1));

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            await ArchiveEligibleEventsAsync(stoppingToken);
        }
    }

    private async Task ArchiveEligibleEventsAsync(CancellationToken ct)
    {
        var configs = await _configStore.GetAllConfigurationsAsync();

        foreach (var config in configs.Where(c => c.Lifecycle.AutoArchive))
        {
            var eligibleEvents = await GetEventsEligibleForArchivalAsync(
                config.StreamName,
                config.Lifecycle.ArchiveAfter);

            await ArchiveEventsAsync(
                eligibleEvents,
                config.Lifecycle.ArchiveLocation,
                config.Lifecycle.ArchiveFormat,
                config.Lifecycle.CompressOnArchive);

            if (config.Lifecycle.AutoDelete)
            {
                await DeleteArchivedEventsAsync(eligibleEvents);
            }
        }
    }
}

Manual Archive Trigger

// Trigger manual archival
var archivalService = serviceProvider.GetRequiredService<IArchivalService>();

await archivalService.ArchiveStreamAsync(
    streamName: "orders",
    fromDate: DateTimeOffset.UtcNow.AddDays(-365),
    toDate: DateTimeOffset.UtcNow.AddDays(-90));

_logger.LogInformation("Manual archival completed");

Restoring from Archive

public class ArchiveRestoreService
{
    public async Task RestoreFromArchiveAsync(
        string streamName,
        DateTimeOffset fromDate,
        DateTimeOffset toDate,
        CancellationToken ct)
    {
        var config = await _configStore.GetConfigurationAsync(streamName);
        var archiveLocation = config.Lifecycle.ArchiveLocation;

        // Download from S3/Azure/File
        var archivedEvents = await DownloadArchivedEventsAsync(
            archiveLocation,
            fromDate,
            toDate,
            ct);

        // Restore to database
        foreach (var @event in archivedEvents)
        {
            await _eventStore.AppendAsync(streamName, @event);
        }

        _logger.LogInformation(
            "Restored {Count} events from archive for {Stream}",
            archivedEvents.Count,
            streamName);
    }
}

Monitoring Lifecycle

// Monitor archival status
var archivalStatus = new
{
    StreamName = "orders",
    TotalEvents = await GetEventCountAsync("orders"),
    ArchivedEvents = await GetArchivedEventCountAsync("orders"),
    EligibleForArchival = await GetArchivalEligibleCountAsync("orders"),
    NextArchivalRun = _archivalService.GetNextRunTime()
};

if (archivalStatus.EligibleForArchival > 10000)
{
    _logger.LogWarning(
        "{Count} events eligible for archival in {Stream}",
        archivalStatus.EligibleForArchival,
        archivalStatus.StreamName);
}

Best Practices

DO

  • Enable auto-create for dynamic stream names
  • Archive to durable storage (S3, Azure Blob)
  • Use Parquet format for analytics
  • Compress archived events
  • Test restore process regularly
  • Document archive locations
  • Monitor archival success/failures
  • Implement archive verification
  • Use appropriate archive timing
  • Keep financial/audit data indefinitely

DON'T

  • Don't delete without archiving critical data
  • Don't use auto-delete for compliance data
  • Don't forget to test restore procedures
  • Don't archive too frequently (adds overhead)
  • Don't use local file system for production archives
  • Don't forget archive access credentials
  • Don't skip compression for large datasets
  • Don't auto-delete before verifying archive

See Also