Idempotency for Subscribers
Overview
Idempotency ensures that event subscribers process each message exactly once, even when the message bus redelivers the same message multiple times. This is critical for maintaining data integrity in distributed systems.
Why Do Messages Get Redelivered?
Message buses like NServiceBus, RabbitMQ, and Azure Service Bus may redeliver messages due to:
Transient Failures
Network timeouts, database connection issues
Process Crashes
Application crashes before acknowledging the message
Retry Policies
Automatic retries on failed message processing
Infrastructure Issues
Message broker restarts, failovers
At-Least-Once Delivery
Most message buses guarantee delivery but may duplicate
What Problems Can Duplicate Processing Cause?
Without idempotency, duplicate message processing can lead to:
📧 Multiple emails sent to the same user
💳 Duplicate financial transactions
📦 Duplicate orders or shipments
🔢 Incorrect counters or statistics
💾 Duplicate database records
🔗 Inconsistent data across systems
Quick Start
FlexBase provides automatic idempotency for subscribers using the Decorator Pattern. No changes are required to your existing subscriber code.
Step 1: Add Configuration to appsettings.json
Step 2: Register Idempotency Services
In your CommonHostConfig.cs or startup configuration:
That's it! All your subscribers now have automatic idempotency protection.
Configuration Options
FlexIdempotency Section
Enabled
true
Enable/disable idempotency checking globally
StoreType
"Cache"
Storage backend: "Cache" or "Database"
DefaultTimeToLiveDays
7
How long to remember processed events
KeyPrefix
"flex:idempotency"
Prefix for cache keys (Cache store only)
EnableAutoCleanup
false
Auto-cleanup expired records (Database store only)
CleanupBatchSize
1000
Records per cleanup batch (Database store only)
FlexCache Section (for Cache Store)
MaxPayloadBytes
1048576 (1MB)
Maximum cache entry size
DefaultExpirationMinutes
60
Default cache entry expiration
Store Types
FlexBase offers two storage backends for tracking processed events:
Option 1: Cache Store (Default)
Uses FlexHybridCache with L1 (in-memory) + L2 (distributed) architecture.
Advantages:
✅ High performance (in-memory L1 cache)
✅ Scales across multiple instances (with distributed L2)
✅ Simple setup for development
Cache Backend Options:
In-Memory Only
Development, single instance
None
Redis
Production, high performance
Requires Redis server
SQL Server
Production, no Redis available
Requires cache table
Option 2: Database Store (EF Core)
Uses your application's Entity Framework Core DbContext to store processed events.
Advantages:
✅ Data persists across application restarts
✅ Queryable (you can analyze processed events)
✅ Uses existing database infrastructure
✅ No additional cache infrastructure needed
Disadvantages:
⚠️ Slower than cache-based storage
⚠️ Increases database load
⚠️ Requires EF Core migration
Cache Store Configuration
Development: In-Memory Only
For local development, no additional configuration is needed. FlexHybridCache uses in-memory storage by default.
⚠️ Warning: In-memory cache is lost on application restart and not shared across instances. Use distributed cache for production.
Production: Redis Backend
Redis provides the best performance for distributed caching.
Step 1: Install NuGet Package
Step 2: Configure Redis
Step 3: Add Connection String
Production: SQL Server Distributed Cache
If you don't have Redis, you can use SQL Server as a distributed cache backend.
Step 1: Install NuGet Package
Step 2: Create the Cache Table
Option A: Using dotnet tool
Option B: Using SQL Script
Step 3: Configure SQL Server Distributed Cache
Step 4: Add Connection String
Database Store Configuration (EF Core)
For full database persistence with a typed entity, use the EF Core store.
Step 1: Configure Store Type
Step 2: Choose Your DbContext Approach
FlexBase provides two options for the Database store:
Dedicated Context
Uses FlexProcessedEventDbContext (provided by FlexBase)
Isolation, separate migrations
Shared Context
Add entity to your ApplicationEFDbContext
Simplicity, single migration stream
Option A: Dedicated Context (Recommended for Isolation)
Use the built-in FlexProcessedEventDbContext. No changes to your application DbContext required.
Register in IdempotencyConfig.cs:
Create migrations:
💡 Tip: Using
--output-dir Idempotencykeeps idempotency migrations separate from your main application migrations.
Option B: Shared Context (Simpler Setup)
Add the FlexProcessedEventEntity to your existing ApplicationEFDbContext. This keeps all entities and migrations together.
Step B1: Modify your ApplicationEFDbContext:
Step B2: Register in IdempotencyConfig.cs:
Step B3: Create migrations:
Which Approach Should I Choose?
Setup complexity
More configuration
Simpler
Migration management
Separate migrations
Single migration stream
Transaction scope
Separate transactions
Same transaction as business entities
Connection pooling
Uses separate pool
Shares connection pool
Schema isolation
Fully isolated
Mixed with app entities
Recommendation:
Use Shared Context for most applications (simpler, fewer moving parts)
Use Dedicated Context if you need schema isolation or want separate database
Database Table Structure
The migration creates a FlexProcessedEvents table:
Id
bigint
Auto-increment primary key
EventId
nvarchar(450)
Unique event identifier
SubscriberType
nvarchar(450)
Fully qualified subscriber type name
CorrelationId
nvarchar(450)
Optional correlation ID for tracing
ProcessedAtUtc
datetime2
When the event was processed
ExpiresAtUtc
datetime2
When the record can be cleaned up
Indexes:
Composite unique index on
(EventId, SubscriberType)Index on
ExpiresAtUtcfor cleanup queries
Cleanup of Expired Records
For Cache Store: Expired records are automatically cleaned up by the cache infrastructure (Redis, HybridCache). No action needed.
For Database Store: Records have an ExpiresAtUtc column and expired records are automatically ignored during idempotency checks. However, you need to periodically clean up expired records to prevent table growth.
FlexBase provides a CleanupExpiredAsync() method you can call from your own scheduled job:
Alternative: Direct SQL cleanup
If you prefer, you can also clean up using a SQL Agent job or scheduled query:
Note: Even without cleanup, expired records don't affect correctness - they are ignored during idempotency checks. Cleanup is only needed to manage table size.
Opting Out of Idempotency
Some subscribers should always run, even on retries (e.g., logging, metrics, notifications). Use the [SkipIdempotency] attribute:
How It Works
FlexBase automatically adds a "gatekeeper" in front of your subscribers. This gatekeeper checks if an event has already been processed before allowing it through to your code.
The key point: Your existing subscriber code doesn't change at all. FlexBase handles everything behind the scenes when you register your services.
What Happens When a Message Arrives
Why Your Code Doesn't Need to Change
When you call AddFlexIdempotency(), FlexBase automatically:
Intercepts all calls to your subscribers
Checks if the
EventIdhas been seen beforeBlocks duplicates from reaching your code
Passes through new messages to your subscriber
Your subscriber just receives messages and processes them - it doesn't know (or care) that there's a gatekeeper in front of it.
Key Points
No code changes required - Your subscribers work exactly as before
EventId-based deduplication - Each event has a unique
EventId(auto-generated)Per-subscriber tracking - The same event can be processed by different subscribers
TTL-based cleanup - Old records are automatically expired after the configured time
Troubleshooting
Events Being Skipped Unexpectedly
Check EventId generation - Events should have unique
EventIdvaluesVerify TTL settings - If TTL is too long, old events may still be "remembered"
Check cache connectivity - Ensure Redis/SQL Server is reachable
Duplicate Processing Still Occurring
Verify registration - Ensure
AddFlexIdempotency()is calledCheck store type - Verify the configured store is working
Check for
[SkipIdempotency]- Subscriber may be opted out
Debug Logging
Enable debug logging to see idempotency decisions:
Log messages:
"Event {EventId} already processed by {Subscriber}; skipping"- Duplicate blocked"Successfully processed event {EventId} by {Subscriber}"- First-time processing
Best Practices
1. Choose the Right Store Type
Development
Cache (in-memory)
Single instance, simple app
Cache (in-memory)
Multi-instance, high throughput
Cache + Redis
Multi-instance, no Redis
Cache + SQL Server
Need queryable history
Database (EF Core)
Regulatory/audit requirements
Database (EF Core)
2. Set Appropriate TTL
Too short: Events may be reprocessed if redelivered after TTL expires
Too long: Storage grows unnecessarily, cleanup slower
Recommended: 7 days (default) for most scenarios
3. Monitor Storage Growth
For Database store, periodically review the FlexProcessedEvents table size and enable EnableAutoCleanup if needed.
4. Use SkipIdempotency Sparingly
Only use [SkipIdempotency] for:
Logging/auditing subscribers
Metrics/telemetry collectors
Notification dispatchers (where duplicates are acceptable)
Complete Example
appsettings.json
IdempotencyConfig.cs
CommonHostConfig.cs
Summary
Purpose
Prevent duplicate event processing
Default Store
Cache (FlexHybridCache)
Alternative Store
Database (EF Core)
Cache Backends
In-memory, Redis, SQL Server
Opt-out
[SkipIdempotency] attribute
Configuration
FlexIdempotency section in appsettings.json
Code Changes Required
None (Decorator Pattern)
Last updated