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/.
As a Brighter user, I would like sample applications demonstrating the error handling actions (DeferMessageAction, RejectMessageAction, DontAckAction) for Kafka and RabbitMQ, so that I can quickly understand how to use this new functionality in my own applications.
Brighter's default behavior is to ack a message when an error leaves the handler pipeline. Errors come in two forms:
- Transient - retried a number of times using
UsePolicyAttributeorUseResilienceAttribute - Non-transient - fail and ack the message, relying on an operator to investigate from the logs
However, Brighter now supports three actions that change this flow by being thrown as exceptions:
| Action | Behavior | Use Case |
|---|---|---|
| DeferMessageAction | Requeue the message with a delay | Transient failures that should retry later |
| RejectMessageAction | Reject the message and put a copy on a dead letter channel | Non-recoverable failures that need operator attention |
| DontAckAction | Nack the message, putting it back on the stream or queue | Feature switches, blocking scenarios, stream reprocessing |
In addition, Brighter provides middleware attributes that wrap a handler to trigger these exceptions when an unhandled exception bubbles out:
| Attribute | Throws | Pipeline Position |
|---|---|---|
[RejectMessageOnError(step: 0)] / [RejectMessageOnErrorAsync(step: 0)] |
RejectMessageAction |
Outermost (step 0) |
[DontAckOnError(step: 0)] / [DontAckOnErrorAsync(step: 0)] |
DontAckAction |
Outermost (step 0) |
[DeferMessageOnError(step: 0)] / [DeferMessageOnErrorAsync(step: 0)] |
DeferMessageAction |
Outermost (step 0) - if available |
These attributes give users a way to select a different default for what happens when the handler exits via an exception (as opposed to falling through, which is treated as success).
Much of this functionality is new and covered by the following specs and ADRs in the main Brighter repository:
RejectMessageAction:
- Specs: 0001-kafka-dead-letter-queue, 0002-backstop-error-handler, 0010-aws-sqs-dead-letter-queue, 0011-redis-dead-letter-queue, 0012-mssql-dead-letter-queue, 0013-postgres-dead-letter-queue, 0014-rocketmq-dead-letter-queue, 0015-mqtt-dead-letter-queue
- ADRs: 0037, 0038, 0039, 0040, 0041, 0042, 0043, 0045, 0046, 0047
DeferMessageAction (Requeue):
- Specs: 0002-universal_scheduler_delay, 0004-transport-scheduler-wiring
- ADRs: 0037-universal-scheduler-delay, 0039-transport-scheduler-wiring
DontAckAction:
- Spec: 0020-DontAckAction
Create a set of sample TaskQueue applications in this repository that demonstrate each error handling action for both Kafka and RabbitMQ. Each sample follows the established Brighter sample pattern (shared library + sender + receiver) and shows a single, focused error-handling scenario.
Six sample applications organized by transport and error action:
- KafkaRejectOnError - Demonstrates
RejectMessageActionwith DLQ routing - KafkaDeferOnError - Demonstrates
DeferMessageActionwith requeue and delay - KafkaDontAckOnError - Demonstrates
DontAckActionwith nack behavior
- RMQRejectOnError - Demonstrates
RejectMessageActionwith DLQ routing - RMQDeferOnError - Demonstrates
DeferMessageActionwith requeue and delay - RMQDontAckOnError - Demonstrates
DontAckActionwith nack behavior
Each sample consists of:
- Shared library (
Greetings/) - Event definitions, handlers with the relevant error attribute, message mappers - Sender (
GreetingsSender/) - Sends messages on a timer - Receiver (
GreetingsReceiverConsole/) - Consumes messages with the error handling behavior - DLQ Consumer (
DlqConsole/) - Only for RejectOnError samples; consumes rejected messages - README.md - Explains what the sample demonstrates, how to run it, and what to expect
- FR-1: Each sample MUST demonstrate exactly one error handling action (Reject, Defer, or DontAck) for one transport (Kafka or RabbitMQ)
- FR-2: Each sample MUST use the corresponding middleware attribute (
[RejectMessageOnErrorAsync],[DontAckOnErrorAsync], or defer equivalent) on the handler at step 0 - FR-3: RejectOnError samples MUST include a DLQ consumer application that reads rejected messages and displays rejection metadata (original topic, rejection reason)
- FR-4: DeferOnError samples MUST show the message being requeued with a visible delay, and the handler eventually succeeding on retry
- FR-5: DontAckOnError samples MUST show the message remaining unacknowledged and being re-delivered by the transport
- FR-6: Each handler MUST simulate a deterministic failure (e.g., every Nth message, or a configurable flag) so the user can observe both success and error paths
- FR-7: Each sample MUST use the
IHostedService/ Generic Host pattern consistent with existing Brighter samples - FR-8: Each sample MUST use
System.Text.Jsonfor serialization viaJsonSerialisationOptions.Options - FR-9: Kafka samples MUST use
MessagePumpType.Proactor(async); RabbitMQ samples MAY use eitherReactororProactor - FR-10: Each sample MUST include a README.md explaining: what the sample demonstrates, prerequisites, how to run it (including Docker), and expected output
- NFR-1: Samples MUST be simple and focused - a new user should understand the pattern within 5 minutes of reading the code
- NFR-2: Samples MUST target
net9.0 - NFR-3: Samples SHOULD use Serilog with console sinks for logging
- NFR-4: Samples MUST reference Brighter packages via NuGet (not project references), using the latest stable version
- NFR-5: All source files MUST include the MIT license header per Brighter conventions
- NFR-6: Docker Compose files MUST be provided for running the required infrastructure (Kafka/Zookeeper, RabbitMQ)
- Samples run against local Docker infrastructure (Kafka on localhost:9092, RabbitMQ on localhost:5672)
- Samples reference Brighter NuGet packages (not source project references, since this is a separate repository)
- The
DeferMessageOnErrorattribute may or may not exist yet - if not available, the DeferOnError samples should throwDeferMessageActiondirectly from the handler to demonstrate the requeue behavior - Each sample is self-contained and independently runnable
- Follows the established Brighter sample naming and structural conventions
- Samples for transports other than Kafka and RabbitMQ (e.g., AWS SQS, Redis, MQTT)
- Production deployment guidance
- Performance testing or benchmarking
- Custom retry policies (samples use simple defaults)
- Web API or ASP.NET Core integration (console apps only)
- Samples for
InvalidMessageAction(deserialization-level concern, not handler-level)
- AC-1: All six sample applications build successfully with
dotnet build - AC-2: Each Kafka sample runs correctly against a local Kafka instance (via Docker Compose)
- AC-3: Each RabbitMQ sample runs correctly against a local RabbitMQ instance (via Docker Compose)
- AC-4: RejectOnError samples: rejected messages appear in the DLQ consumer with correct metadata
- AC-5: DeferOnError samples: deferred messages are requeued and eventually processed after the delay
- AC-6: DontAckOnError samples: nacked messages are re-delivered by the transport and visible in logs
- AC-7: Each README.md provides clear, accurate instructions for running the sample
- AC-8: A top-level README.md provides an overview of all samples and links to each
The main Brighter repository contains these relevant samples:
samples/TaskQueue/KafkaTaskQueue/- Basic Kafka send/receivesamples/TaskQueue/KafkaTaskQueueWithDLQ/- Kafka with RejectMessageOnError and DLQ consumersamples/TaskQueue/RMQTaskQueue/- Basic RabbitMQ send/receive
| Behavior | Kafka | RabbitMQ |
|---|---|---|
| Nack | No-op (not committing offset is sufficient) | BasicNack with requeue=true |
| DLQ | Brighter-managed (produce to DLQ topic) | Can use native DLX or Brighter-managed |
| Requeue with delay | Via scheduler/producer delegation | Via scheduler/producer delegation |
These transport differences should be highlighted in each sample's README.