Note: This document captures user requirements and needs. Technical design decisions and implementation details should be documented in an Architecture Decision Record (ADR) in
docs/adr/.
Linked Issue: #4115
As a developer using Brighter with the default InMemoryOutbox, I would like to configure the entry limit and compaction behaviour, so that messages are not silently compacted and lost before they can be dispatched.
The InMemoryBox base class (used by both InMemoryInbox and InMemoryOutbox) has a hard-coded default limit of 2048 entries. When this limit is reached, the oldest entries are compacted (removed). While the EntryLimit property is publicly settable, when InMemoryOutbox is created automatically by ServiceCollectionExtensions.AddProducers (because no explicit outbox was supplied), there is no way to configure it — the defaults are baked in.
At scale, 2048 entries can be exceeded before messages are dispatched, causing:
- Messages silently compacted from the outbox before dispatch.
OutboxProducerMediator.ClearOutboxthrows aNullReferenceExceptionwhen it cannot find compacted messages — an inappropriate exception type with a misleading message.
Allow users to configure the InMemoryOutbox defaults via ProducersConfiguration without requiring them to construct and register an InMemoryOutbox themselves. Additionally, make the compaction/expiry strategy smarter for outboxes vs inboxes, and improve error handling when messages are missing.
- Configurable default outbox via ProducersConfiguration: Add an
InMemoryBoxConfigurationrecord to collateEntryLimit,EntryTimeToLive,ExpirationScanInterval, andCompactionPercentage. Expose it asDefaultBoxConfigurationonProducersConfigurationso users can configure the auto-createdInMemoryOutbox. - Support unlimited entries (EntryLimit = -1): When
EntryLimitis set to-1, compaction must not run. Expiry-based cleanup should still run. - Make InMemoryBox abstract:
InMemoryBoxshould become an abstract class withRemoveExpiredMessagesandCompactas abstract methods, forcing each subclass to define its own expiry and compaction strategies. - Outbox-aware eviction:
InMemoryOutboxshould override bothRemoveExpiredMessagesandCompactto only remove entries whereTimeFlushedis notDateTimeOffset.MinValue(i.e. the message has been dispatched). Undispatched messages must never be removed by either expiry or compaction, regardless of age. - Inbox retains existing behaviour:
InMemoryInboxshould overrideRemoveExpiredMessagesandCompactwith the existing algorithms (remove entries older thanEntryTimeToLivefor expiry; remove oldest byWriteTimefor compaction), unlessEntryLimitis-1. - Graceful handling of missing messages: When
OutboxProducerMediator.ClearOutboxcannot find a message in the outbox, it should log an error with actionable context rather than throwing aNullReferenceException.
- Backwards compatible: Existing behaviour must be preserved when no new configuration is provided. The default
EntryLimitremains 2048. - Thread safety: All changes to compaction and expiry must remain thread-safe, consistent with the existing
Monitor-based locking inInMemoryBox. - Performance: Expiry scans in
InMemoryOutboxshould not introduce measurable overhead compared to the current implementation.
- The
InMemoryOutboxis intended for development, testing, and low-to-moderate throughput production scenarios. Users with very high throughput should use a persistent outbox (SQL, DynamoDB, etc.). InMemoryBoxis currently concrete with a non-virtualRemoveExpiredMessages— it will become abstract, withRemoveExpiredMessagesas aprotected abstractmethod. BothInMemoryOutboxandInMemoryInboxmust provide implementations.- The fix for
NullReferenceExceptioninOutboxProducerMediatoris a separate, independent change that should not be gated on the configuration work.
- Configuring the
InMemoryInboxvia DI extensions (can be a follow-up). - Adding persistent outbox implementations.
- Changing the compaction algorithm itself (e.g. LRU eviction).
- Metrics or health checks for outbox capacity.
- A user can set
DefaultBoxConfigurationonProducersConfigurationwith anInMemoryBoxConfigurationrecord and have those values applied to the auto-createdInMemoryOutbox. - Setting
EntryLimit = -1disables compaction; expiry still removes dispatched messages. InMemoryOutboxnever removes undispatched messages during expiry scans.InMemoryInboxbehaviour is unchanged.- When
OutboxProducerMediatorcannot find a message, it logs an error (not throwsNullReferenceException). - All existing tests continue to pass.
- New tests cover: configurable limits via DI,
-1entry limit, outbox-aware expiry, missing-message error handling.
Relevant source files:
src/Paramore.Brighter/InMemoryBox.cs— base class withEntryLimit(default 2048), compaction, and expiry logicsrc/Paramore.Brighter/InMemoryOutbox.cs— outbox implementation withOutboxEntry.TimeFlushedsrc/Paramore.Brighter/InMemoryInbox.cs— inbox implementationsrc/Paramore.Brighter/ProducersConfiguration.cs— configuration class that needs new propertiessrc/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs(lines ~296, ~430) — where defaultInMemoryOutboxis createdsrc/Paramore.Brighter/OutboxProducerMediator.cs(lines ~311-315, ~375-379) — whereNullReferenceExceptionis thrown for missing messages