SourceFlow.Net provides flexible idempotency configuration for cloud-based deployments to handle duplicate messages in distributed systems. This guide explains how to configure idempotency services for AWS cloud integration, covering both in-memory and SQL-based approaches.
Purpose: Prevent duplicate message processing in distributed systems where at-least-once delivery guarantees can result in duplicate messages.
- Understanding Idempotency
- Idempotency Approaches
- In-Memory Idempotency
- SQL-Based Idempotency
- Configuration Methods
- Fluent Builder API
- Cloud Message Handling
- Performance Considerations
- Best Practices
- Troubleshooting
Idempotency ensures that processing the same message multiple times produces the same result as processing it once. This is critical in distributed systems where:
- Cloud messaging services guarantee at-least-once delivery
- Network failures can cause message retries
- Multiple consumers might receive the same message
Message Received
↓
Generate Idempotency Key
↓
Check if Already Processed
↓
If Duplicate → Skip Processing
If New → Process and Mark as Processed
Pattern: {CloudProvider}:{MessageType}:{MessageId}
Example: AWS:CreateOrderCommand:abc123-def456
SourceFlow provides two idempotency implementations:
Implementation: InMemoryIdempotencyService
Storage: ConcurrentDictionary<string, DateTime>
Use Cases:
- Single-instance deployments
- Development and testing environments
- Local development with LocalStack
Pros:
- ✅ Zero configuration
- ✅ Fastest performance
- ✅ No external dependencies
Cons:
- ❌ Not shared across instances
- ❌ Lost on application restart
- ❌ Not suitable for production multi-instance deployments
Implementation: EfIdempotencyService
Storage: Database table (IdempotencyRecords)
Use Cases:
- Multi-instance production deployments
- Horizontal scaling scenarios
- High-availability configurations
Pros:
- ✅ Shared across all instances
- ✅ Survives application restarts
- ✅ Supports horizontal scaling
- ✅ Automatic cleanup
Cons:
⚠️ Requires database setup⚠️ Slightly slower than in-memory (still fast)
By default, SourceFlow automatically registers an in-memory idempotency service when you configure AWS cloud integration.
services.UseSourceFlow();
services.UseSourceFlowAws(
options => { options.Region = RegionEndpoint.USEast1; },
bus => bus
.Send.Command<CreateOrderCommand>(q => q.Queue("orders.fifo"))
.Listen.To.CommandQueue("orders.fifo"));
// InMemoryIdempotencyService registered automatically// Internal implementation (simplified)
public class InMemoryIdempotencyService : IIdempotencyService
{
private readonly ConcurrentDictionary<string, DateTime> _processedMessages = new();
public Task<bool> HasProcessedAsync(string idempotencyKey)
{
if (_processedMessages.TryGetValue(idempotencyKey, out var expiresAt))
{
return Task.FromResult(DateTime.UtcNow < expiresAt);
}
return Task.FromResult(false);
}
public Task MarkAsProcessedAsync(string idempotencyKey, TimeSpan ttl)
{
_processedMessages[idempotencyKey] = DateTime.UtcNow.Add(ttl);
return Task.CompletedTask;
}
}Expired entries are automatically removed from memory when checked.
The SQL-based idempotency service (EfIdempotencyService) provides distributed duplicate message detection using a database to track processed messages across multiple application instances.
public class IdempotencyRecord
{
public string IdempotencyKey { get; set; } // Primary key
public DateTime ProcessedAt { get; set; } // When first processed
public DateTime ExpiresAt { get; set; } // Expiration timestamp
public string MessageType { get; set; } // Optional: message type
public string CloudProvider { get; set; } // Optional: cloud provider
}- Manages the
IdempotencyRecordstable - Configures primary key on
IdempotencyKey - Adds index on
ExpiresAtfor efficient cleanup
Implements IIdempotencyService with:
- HasProcessedAsync: Checks if message processed (not expired)
- MarkAsProcessedAsync: Records message as processed with TTL
- RemoveAsync: Deletes specific idempotency record
- GetStatisticsAsync: Returns processing statistics
- CleanupExpiredRecordsAsync: Batch cleanup of expired records
Background hosted service that periodically cleans up expired records.
CREATE TABLE IdempotencyRecords (
IdempotencyKey NVARCHAR(500) PRIMARY KEY,
ProcessedAt DATETIME2 NOT NULL,
ExpiresAt DATETIME2 NOT NULL,
MessageType NVARCHAR(500) NULL,
CloudProvider NVARCHAR(50) NULL
);
CREATE INDEX IX_IdempotencyRecords_ExpiresAt
ON IdempotencyRecords(ExpiresAt);dotnet add package SourceFlow.Stores.EntityFrameworkservices.AddSourceFlowIdempotency(
connectionString: "Server=localhost;Database=SourceFlow;Trusted_Connection=True;",
cleanupIntervalMinutes: 60); // Optional, defaults to 60 minutesThis method:
- Registers
IdempotencyDbContextwith SQL Server provider - Registers
EfIdempotencyServiceas scoped service - Registers
IdempotencyCleanupServiceas background hosted service - Configures automatic cleanup at specified interval
For PostgreSQL, MySQL, SQLite, or other EF Core providers:
// PostgreSQL
services.AddSourceFlowIdempotencyWithCustomProvider(
configureContext: options => options.UseNpgsql(connectionString),
cleanupIntervalMinutes: 60);
// MySQL
services.AddSourceFlowIdempotencyWithCustomProvider(
configureContext: options => options.UseMySql(
connectionString,
ServerVersion.AutoDetect(connectionString)),
cleanupIntervalMinutes: 60);
// SQLite
services.AddSourceFlowIdempotencyWithCustomProvider(
configureContext: options => options.UseSqlite(connectionString),
cleanupIntervalMinutes: 60);- Uses database transactions for atomic operations
- Handles race conditions with upsert pattern
- Detects duplicate key violations across DB providers
- Background service runs at configurable intervals
- Batch deletion of expired records (1000 per cycle)
- Prevents unbounded table growth
- Shared database ensures consistency across instances
- No in-memory state required
- Scales horizontally with application
- Total checks performed
- Duplicates detected
- Unique messages processed
- Current cache size
The EfIdempotencyService is registered as Scoped to match the lifetime of cloud dispatchers:
- Command dispatchers are scoped (transaction boundaries)
- Event dispatchers are singleton but create scoped instances
- Scoped lifetime ensures proper DbContext lifecycle management
Register the idempotency service before configuring AWS, and it will be automatically detected:
services.UseSourceFlow();
// Register Entity Framework stores and SQL-based idempotency
services.AddSourceFlowEfStores(connectionString);
services.AddSourceFlowIdempotency(
connectionString: connectionString,
cleanupIntervalMinutes: 60);
// Configure AWS - will automatically use registered EF idempotency service
services.UseSourceFlowAws(
options => { options.Region = RegionEndpoint.USEast1; },
bus => bus
.Send.Command<CreateOrderCommand>(q => q.Queue("orders.fifo"))
.Listen.To.CommandQueue("orders.fifo"));Use the optional configureIdempotency parameter:
services.UseSourceFlow();
// Register Entity Framework stores
services.AddSourceFlowEfStores(connectionString);
// Configure AWS with explicit idempotency configuration
services.UseSourceFlowAws(
options => { options.Region = RegionEndpoint.USEast1; },
bus => bus
.Send.Command<CreateOrderCommand>(q => q.Queue("orders.fifo"))
.Listen.To.CommandQueue("orders.fifo"),
configureIdempotency: services =>
{
services.AddSourceFlowIdempotency(connectionString, cleanupIntervalMinutes: 60);
});Provide a custom idempotency implementation:
services.UseSourceFlowAws(
options => { options.Region = RegionEndpoint.USEast1; },
bus => bus.Send.Command<CreateOrderCommand>(q => q.Queue("orders.fifo")),
configureIdempotency: services =>
{
services.AddScoped<IIdempotencyService, MyCustomIdempotencyService>();
});- UseSourceFlowAws is called with optional
configureIdempotencyparameter - If
configureIdempotencyparameter is provided, it's executed to register the idempotency service - If
configureIdempotencyis null, checks ifIIdempotencyServiceis already registered - If not registered, registers
InMemoryIdempotencyServiceas default
SourceFlow provides a fluent IdempotencyConfigurationBuilder for more expressive configuration.
Important: The UseEFIdempotency method requires the SourceFlow.Stores.EntityFramework package. The builder uses reflection to avoid a direct dependency in the core package.
// First, ensure the package is installed:
// dotnet add package SourceFlow.Stores.EntityFramework
var idempotencyBuilder = new IdempotencyConfigurationBuilder()
.UseEFIdempotency(connectionString, cleanupIntervalMinutes: 60);
// Apply configuration to service collection
idempotencyBuilder.Build(services);
// Then configure cloud provider
services.UseSourceFlowAws(
options => { options.Region = RegionEndpoint.USEast1; },
bus => bus.Send.Command<CreateOrderCommand>(q => q.Queue("orders.fifo")));If the EntityFramework package is not installed, you'll receive a clear error message:
SourceFlow.Stores.EntityFramework package is not installed.
Install it using: dotnet add package SourceFlow.Stores.EntityFramework
var idempotencyBuilder = new IdempotencyConfigurationBuilder()
.UseInMemory();
idempotencyBuilder.Build(services);// With type parameter
var idempotencyBuilder = new IdempotencyConfigurationBuilder()
.UseCustom<MyCustomIdempotencyService>();
// Or with factory function
var idempotencyBuilder = new IdempotencyConfigurationBuilder()
.UseCustom(provider =>
{
var logger = provider.GetRequiredService<ILogger<MyCustomIdempotencyService>>();
return new MyCustomIdempotencyService(logger);
});
idempotencyBuilder.Build(services);| Method | Description | Use Case |
|---|---|---|
UseEFIdempotency(connectionString, cleanupIntervalMinutes) |
Configure Entity Framework-based idempotency (uses reflection) | Multi-instance production deployments |
UseInMemory() |
Configure in-memory idempotency | Single-instance or development environments |
UseCustom<TImplementation>() |
Register custom implementation by type | Custom idempotency logic with DI |
UseCustom(factory) |
Register custom implementation with factory | Custom idempotency with complex initialization |
Build(services) |
Apply configuration to service collection (uses TryAddScoped) | Final step to register services |
- Reflection-Based EF Integration:
UseEFIdempotencyuses reflection to callAddSourceFlowIdempotencyfrom the EntityFramework package - Lazy Registration: The
Buildmethod only registers services if no configuration was set, usingTryAddScoped - Error Handling: Clear error messages guide users when required packages are missing
- Service Lifetime: All idempotency services are registered as Scoped to match dispatcher lifetimes
- Explicit Configuration: Clear, readable idempotency setup
- Reusable: Create builder instances for different environments
- Testable: Easy to mock and test configuration logic
- Type-Safe: Compile-time validation of configuration
- Flexible: Mix and match with direct service registration
// In AwsSqsCommandListener
var idempotencyKey = GenerateIdempotencyKey(message);
if (await idempotencyService.HasProcessedAsync(idempotencyKey))
{
// Duplicate detected - skip processing
await DeleteMessage(message);
return;
}
// Process message
await commandBus.Publish(command);
// Mark as processed
await idempotencyService.MarkAsProcessedAsync(idempotencyKey, ttl);Default TTL: 5 minutes
Configurable per message type:
// Short TTL for high-frequency messages
await idempotencyService.MarkAsProcessedAsync(key, TimeSpan.FromMinutes(2));
// Longer TTL for critical operations
await idempotencyService.MarkAsProcessedAsync(key, TimeSpan.FromMinutes(15));The SQL-based idempotency service includes a background cleanup service that:
- Runs at configurable intervals (default: 60 minutes)
- Deletes expired records in batches (1000 per cycle)
- Prevents unbounded table growth
- Runs independently without blocking message processing
- Lookup: O(1) dictionary lookup
- Memory: Minimal overhead per message
- Cleanup: Automatic on access
- Primary key on
IdempotencyKeyfor fast lookups - Index on
ExpiresAtfor efficient cleanup queries
- Batch deletion (1000 records per cycle)
- Configurable cleanup interval
- Runs in background without blocking message processing
- Uses Entity Framework Core connection pooling
- Scoped lifetime matches dispatcher lifetime
- Efficient resource utilization
| Operation | In-Memory | SQL-Based |
|---|---|---|
| Lookup | < 1 ms | 1-5 ms |
| Insert | < 1 ms | 2-10 ms |
| Cleanup | Automatic | Background (60 min) |
| Throughput | 100k+ msg/sec | 10k+ msg/sec |
Use in-memory idempotency for simplicity:
services.UseSourceFlowAws(
options => { options.Region = RegionEndpoint.USEast1; },
bus => bus.Send.Command<CreateOrderCommand>(q => q.Queue("orders.fifo")));
// In-memory idempotency registered automaticallyUse SQL-based idempotency for reliability:
services.AddSourceFlowEfStores(connectionString);
services.AddSourceFlowIdempotency(connectionString, cleanupIntervalMinutes: 60);
services.UseSourceFlowAws(
options => { options.Region = RegionEndpoint.USEast1; },
bus => bus.Send.Command<CreateOrderCommand>(q => q.Queue("orders.fifo")));Use environment-specific configuration:
var connectionString = configuration.GetConnectionString("SourceFlow");
var cleanupInterval = configuration.GetValue<int>("SourceFlow:IdempotencyCleanupMinutes", 60);
if (environment.IsProduction())
{
services.AddSourceFlowIdempotency(connectionString, cleanupInterval);
}
// Development uses in-memory by default- Connection String: Use the same database as your command/entity stores for consistency
- Cleanup Interval: Set based on your TTL values (typically 1-2 hours)
- TTL Values: Match your message retention policies (typically 5-15 minutes)
- Monitoring: Track statistics to understand duplicate message rates
- Database Maintenance: Ensure indexes are maintained for optimal performance
Symptoms: Many messages marked as duplicates
Solutions:
- Check message TTL values (should match your processing time)
- Verify cloud provider retry settings
- Review message deduplication configuration (SQS ContentBasedDeduplication)
- Check for application restarts causing message reprocessing
Symptoms: IdempotencyRecords table growing unbounded
Solutions:
- Verify background service is registered (
IdempotencyCleanupService) - Check application logs for cleanup errors
- Ensure database permissions allow DELETE operations
- Verify cleanup interval is appropriate
- Check that the hosted service is starting correctly
Symptoms: Slow message processing
Solutions:
- Verify indexes exist on
IdempotencyKeyandExpiresAt - Consider increasing cleanup interval
- Monitor database connection pool usage
- Check for database locks or contention
- Review query execution plans
Symptoms: Messages processed again after application restart
Expected Behavior:
- In-Memory: This is expected - state is lost on restart
- SQL-Based: Should not happen - check database connectivity
Solutions:
- Use SQL-based idempotency for production
- Ensure database is accessible during startup
- Verify connection string is correct
Steps:
- Add the SQL-based service registration:
services.AddSourceFlowIdempotency(connectionString);-
Ensure database exists and is accessible
-
The
IdempotencyRecordstable will be created automatically on first use -
No code changes required in dispatchers or listeners
-
Deploy to all instances simultaneously to avoid mixed behavior
| Feature | In-Memory | SQL-Based |
|---|---|---|
| Single Instance | ✅ Excellent | ✅ Works |
| Multi-Instance | ❌ Not supported | ✅ Excellent |
| Performance | ⚡ Fastest | 🔥 Fast |
| Persistence | ❌ Lost on restart | ✅ Survives restarts |
| Cleanup | ✅ Automatic (memory) | ✅ Automatic (background service) |
| Setup Complexity | ✅ Zero config | |
| Scalability | ❌ Single instance only | ✅ Horizontal scaling |
| Database Required | ❌ No | ✅ Yes |
| Package Required | ❌ No | ✅ SourceFlow.Stores.EntityFramework |
- AWS Cloud Architecture
- AWS Cloud Extension Package
- Entity Framework Stores
- Cloud Integration Testing
Document Version: 2.0
Last Updated: 2026-03-04
Status: Complete