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:
Network timeouts, database connection issues
Application crashes before acknowledging the message
Automatic retries on failed message processing
Message broker restarts, failovers
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
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
Setting
Default
Description
Enable/disable idempotency checking globally
Storage backend: "Cache" or "Database"
How long to remember processed events
Prefix for cache keys (Cache store only)
Auto-cleanup expired records (Database store only)
Records per cleanup batch (Database store only)
FlexCache Section (for Cache Store)
Setting
Default
Description
Default cache entry expiration
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:
Backend
Use Case
Setup Complexity
Development, single instance
Production, high performance
Production, no Redis available
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 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 4: Add Connection String
Database Store Configuration (EF Core)
For full database persistence with a typed entity, use the EF Core store.
Step 2: Choose Your DbContext Approach
FlexBase provides two options for the Database store:
Approach
Description
Best For
Uses FlexProcessedEventDbContext (provided by FlexBase)
Isolation, separate migrations
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 Idempotency keeps 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?
Consideration
Dedicated Context
Shared Context
Same transaction as business 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:
Auto-increment primary key
Fully qualified subscriber type name
Optional correlation ID for tracing
When the event was processed
When the record can be cleaned up
Indexes:
Composite unique index on (EventId, SubscriberType)
Index on ExpiresAtUtc for 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:
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 EventId has been seen before
Blocks 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.
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 EventId values
Verify 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 called
Check store type - Verify the configured store is working
Check for [SkipIdempotency] - Subscriber may be opted out
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
1. Choose the Right Store Type
Scenario
Recommended Store
Single instance, simple app
Multi-instance, high throughput
Regulatory/audit requirements
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
Prevent duplicate event processing
In-memory, Redis, SQL Server
[SkipIdempotency] attribute
FlexIdempotency section in appsettings.json