| id | 0051-error-handling-examples | ||||
|---|---|---|---|---|---|
| title | Error Handling Example Applications | ||||
| status | Accepted | ||||
| author |
|
||||
| created | 2026-02-23 | ||||
| summary | Defines six sample TaskQueue applications demonstrating RejectOnError, DeferOnError, and DontAckOnError error-handling actions for Kafka and RabbitMQ, each following the established Brighter sample convention with Greetings, GreetingsSender, GreetingsReceiverConsole, and optional DlqConsole sub-projects. | ||||
| tags |
|
Date: 2026-02-23
Accepted
Parent Requirement: specs/0021-Error-Examples/requirements.md
Scope: This ADR covers the design of six sample TaskQueue applications that demonstrate Brighter's error handling actions (RejectMessageAction, DeferMessageAction, DontAckAction) for Kafka and RabbitMQ transports.
Brighter now provides three exception-based actions that change how the message pump handles errors:
- RejectMessageAction — reject the message, route a copy to a dead letter channel
- DeferMessageAction — requeue the message with a delay for later retry
- DontAckAction — leave the message unacknowledged so the transport re-delivers it
Each action has a corresponding middleware attribute ([RejectMessageOnErrorAsync], [DontAckOnErrorAsync]) that wraps the handler pipeline and converts unhandled exceptions into the appropriate action. The DeferMessageOnError attribute does not yet exist, so the defer samples will throw DeferMessageAction directly.
Users need concrete, runnable examples to understand how to configure and use each action. The existing KafkaTaskQueueWithDLQ sample demonstrates reject-on-error for Kafka. We need four additional samples (Kafka defer, Kafka dont-ack, RMQ reject, RMQ defer, RMQ dont-ack) and should preserve the existing Kafka DLQ sample as the sixth.
- Samples must be simple enough for a new user to understand in minutes
- Each sample must focus on a single error-handling behavior
- Kafka and RabbitMQ have different transport semantics for nack, DLQ, and requeue
- The
DeferMessageOnErrorattribute does not exist yet - Samples live in the main Brighter repository and use project references
- Existing samples establish conventions that must be followed
We will create four new sample applications under samples/TaskQueue/ and keep the existing KafkaTaskQueueWithDLQ sample as-is, giving us five focused error-handling examples. A sixth (RMQRejectOnError) completes the matrix.
┌──────────────────┬──────────────────────────┬──────────────────────────┐
│ Error Action │ Kafka │ RabbitMQ │
├──────────────────┼──────────────────────────┼──────────────────────────┤
│ RejectOnError │ KafkaTaskQueueWithDLQ │ RMQTaskQueueWithDLQ │
│ │ (exists) │ (new) │
├──────────────────┼──────────────────────────┼──────────────────────────┤
│ DeferOnError │ KafkaDeferOnError │ RMQDeferOnError │
│ │ (new) │ (new) │
├──────────────────┼──────────────────────────┼──────────────────────────┤
│ DontAckOnError │ KafkaDontAckOnError │ RMQDontAckOnError │
│ │ (new) │ (new) │
└──────────────────┴──────────────────────────┴──────────────────────────┘
Each sample follows the same structural pattern established by existing Brighter samples:
samples/TaskQueue/{SampleName}/
├── Greetings/ # Shared library (class library)
│ ├── Greetings.csproj
│ └── Ports/
│ ├── Commands/
│ │ └── GreetingEvent.cs # Event definition
│ ├── CommandHandlers/
│ │ └── GreetingEventHandlerAsync.cs # Handler with error attribute
│ └── Mappers/
│ └── GreetingEventMessageMapperAsync.cs # Serialization
├── GreetingsSender/ # Producer (console app)
│ ├── GreetingsSender.csproj
│ ├── Program.cs # Host configuration + producer setup
│ └── TimedMessageGenerator.cs # IHostedService that sends messages
├── GreetingsReceiverConsole/ # Consumer (console app)
│ ├── GreetingsReceiverConsole.csproj
│ └── Program.cs # Host configuration + subscription setup
├── DlqConsole/ # DLQ consumer (RejectOnError only)
│ ├── DlqConsole.csproj
│ ├── Program.cs
│ └── DlqGreetingEventHandlerAsync.cs
└── README.md
Carries the greeting payload. Identical across all samples — inherits from Event, has a Greeting string property. Uses Id.Random() for message identity.
The focal point of each sample. Responsible for:
- Knowing: Which error action this sample demonstrates
- Doing: Processing messages and simulating deterministic failures
- Deciding: When to fail (every Nth message) to show the error path
The handler's attribute decoration is what differs across samples:
| Sample Type | Attribute | Failure Simulation |
|---|---|---|
| RejectOnError | [RejectMessageOnErrorAsync(step: 0)] |
Throw exception on every 5th message |
| DeferOnError | None (throws DeferMessageAction directly) |
Throw DeferMessageAction on every 3rd message, succeed after N retries |
| DontAckOnError | [DontAckOnErrorAsync(step: 0)] |
Throw exception on every 5th message |
Maps between GreetingEvent and Brighter's Message type using System.Text.Json. Kafka samples set PartitionKey; RabbitMQ samples do not.
IHostedService that sends a GreetingEvent every 500ms via IAmACommandProcessor.PostAsync. Identical across all samples.
Processes rejected messages from the DLQ. Displays rejection metadata from Context.Bag (OriginalTopic, RejectionReason).
Producer (GreetingsSender/Program.cs):
KafkaProducerRegistryFactorywithlocalhost:9092KafkaPublication<GreetingEvent>with 3 partitionsInMemorySchedulerFactoryfor scheduler support
Consumer (GreetingsReceiverConsole/Program.cs):
KafkaSubscription<GreetingEvent>withMessagePumpType.ProactorgroupIdunique per sample to avoid consumer group conflictsoffsetDefault: AutoOffsetReset.Earliest- RejectOnError:
deadLetterRoutingKey: new RoutingKey("{topic}.dlq") - DeferOnError:
InMemorySchedulerFactoryfor requeue delay support
Nack behavior: Kafka nack is a no-op — not committing the offset is sufficient. The message pump applies a configurable delay before re-reading.
Producer (GreetingsSender/Program.cs):
RmqProducerRegistryFactorywithamqp://guest:guest@localhost:5672- Exchange:
paramore.brighter.exchange InMemorySchedulerFactoryfor scheduler support
Consumer (GreetingsReceiverConsole/Program.cs):
RmqSubscription<GreetingEvent>withMessagePumpType.ProactorisDurable: true,highAvailability: true- RejectOnError:
deadLetterChannelNameanddeadLetterRoutingKeyconfigured - DeferOnError:
InMemorySchedulerFactoryfor requeue delay support - Uses
Paramore.Brighter.MessagingGateway.RMQ.Asyncgateway
Nack behavior: RabbitMQ performs BasicNack(deliveryTag, requeue: true) — the message is immediately returned to the queue.
Since DeferMessageOnErrorAttribute does not exist, the defer samples throw DeferMessageAction directly from the handler. To demonstrate "eventual success after retry", the handler uses a static counter per message ID:
Handler receives message
→ Is this a "fail" message (every 3rd)?
→ Has this message been seen fewer than N times?
YES → throw new DeferMessageAction("Transient failure, will retry")
NO → Process successfully (simulating transient error clearing)
→ Not a "fail" message → Process normally
This shows the user that deferred messages come back and eventually succeed.
All samples use project references to Brighter source projects (not NuGet packages), consistent with existing samples:
Paramore.Brighter— core frameworkParamore.Brighter.ServiceActivator— message pumpParamore.Brighter.ServiceActivator.Extensions.DependencyInjection— DI integrationParamore.Brighter.ServiceActivator.Extensions.Hosting—ServiceActivatorHostedServiceParamore.Brighter.Extensions.DependencyInjection—AddBrighter()extensionParamore.Brighter.MessagingGateway.Kafka— Kafka transportParamore.Brighter.MessagingGateway.RMQ.Async— RabbitMQ async transport
Samples use existing Docker Compose files already in the repository:
- Kafka:
docker-compose-kafka.yaml(Zookeeper + Kafka broker on port 9092) - RabbitMQ:
docker-compose-rmq.yaml(RabbitMQ with management on ports 5672/15672)
No new Docker Compose files are needed.
Each sample's README follows this structure:
- What This Demonstrates — one-sentence summary of the error action
- How It Works — brief explanation of the error flow with a diagram
- Prerequisites — Docker, .NET 9 SDK
- Running the Sample — step-by-step commands (docker compose up, dotnet run for each app)
- Expected Output — what the user should see in each terminal
- Key Code — pointer to the handler file with the relevant attribute/throw
A top-level samples/TaskQueue/README.md will be added (or updated) linking to all error-handling samples as a group.
- Users have a clear, focused example for each error action on each transport
- Following existing sample conventions means users familiar with one sample can navigate all of them
- Deterministic failure patterns make the behavior observable and reproducible
- Samples serve as integration tests for the error-handling features
- Five new project directories add maintenance burden (code must stay in sync with framework changes)
- The DeferOnError samples use a workaround (direct throw) since the attribute doesn't exist yet — these will need updating when
DeferMessageOnErrorAttributeis added - Some code duplication across samples (GreetingEvent, mapper, TimedMessageGenerator) — this is intentional for sample independence but increases maintenance surface
| Risk | Mitigation |
|---|---|
| Samples drift out of sync with framework | Include samples in CI build; README links to relevant ADRs |
| DeferOnError workaround confuses users | README clearly documents that the attribute is planned; handler code comments explain the direct throw |
| Consumer group ID collisions between samples | Each sample uses a unique, descriptive groupId |
One sample that demonstrates all three actions via configuration. Rejected because it violates the "simple and focused" requirement — users would need to understand all three actions simultaneously.
Since this is the main Brighter repo, using NuGet would mean samples lag behind source. Project references keep samples in sync with the framework code and are consistent with all existing samples.
Creating a standalone repo was considered but rejected because the existing samples already live in the main repo, and project references provide immediate consistency.
- Requirements: specs/0021-Error-Examples/requirements.md
- Related ADRs:
- Existing sample:
samples/TaskQueue/KafkaTaskQueueWithDLQ/ - Existing sample:
samples/TaskQueue/RMQTaskQueue/