Skip to content

feat(eventbus): add retry backoff and dead-letter routing with replay - #204

Merged
tiana-code merged 1 commit into
mainfrom
feat/E-03-retry-dlq
Jun 18, 2026
Merged

feat(eventbus): add retry backoff and dead-letter routing with replay#204
tiana-code merged 1 commit into
mainfrom
feat/E-03-retry-dlq

Conversation

@tiana-code

Copy link
Copy Markdown
Owner

What

Consumer-side error topology for E-03: bounded retry with backoff, terminal dead-letter routing for poison messages, and a replay path. Generic building blocks in libs/fincore-eventbus; a concrete @KafkaListener wiring lands with the first consumer (E-02).

  • RetryDlqProperties (fincore.eventbus.retry): max attempts, exponential backoff (initial/multiplier/max), retry/dead-letter suffixes; validated.
  • RetryTopicNaming - pure deadLetterTopic(base) / retryTopic(base, tier) conventions.
  • RetryDlqConfiguration - a DefaultErrorHandler with a bounded ExponentialBackOff (maxAttempts - 1 retries, since the initial delivery is attempt 1) + a DeadLetterPublishingRecoverer that routes an exhausted record to <topic><dlt-suffix>. The route uses partition -1 so the producer places the record by key, preserving affinity without assuming equal partition counts. A consumer attaches the handler to its container factory.
  • DeadLetterReplayer - re-drives a dead-letter topic back to a target topic, preserving keys and awaiting each send so the returned count is confirmed (incident-recovery safe). Non-destructive (re-driving duplicates is safe with the idempotent consumer from F-03.5 Idempotent consumer base class / annotation #192).

All beans are @ConditionalOnMissingBean and load only when the bus is configured (@Import into the @ConditionalOnProperty(bootstrap-servers) auto-config).

Gates

Local (-x integrationTest, Docker down in WSL): :libs:fincore-eventbus:{test,detekt,detektTest,spotlessCheck,assemble,compileIntegrationTestKotlin} green. The Testcontainers-Redpanda IT runs on CI: the recoverer routes a failed record to its -dlt topic, and the replayer re-drives -dlt back to the base topic preserving keys. critic GO (Spring 6.2 ExponentialBackOff.maxAttempts fix applied), code-reviewer APPROVE (replayer-awaits-futures must-fix applied), security-auditor PASS, evaluator 0.89.

Closes #193

Add a consumer error topology: a DefaultErrorHandler with bounded exponential
backoff that, once retries are exhausted, routes the record to its dead-letter
topic via a DeadLetterPublishingRecoverer, so a poison message can never block a
partition. RetryTopicNaming derives the retry and dead-letter topic names from
configurable suffixes; the dead-letter route uses partition -1 so the producer
places the record by key, preserving per-key affinity. DeadLetterReplayer re-drives
a dead-letter topic back to a target topic preserving keys and awaiting each send,
so the returned count reflects confirmed re-publishes. All beans are
conditional-on-missing and load only when the bus is configured. Covered by unit
tests plus a Testcontainers-Redpanda integration test.

Closes #193
@tiana-code tiana-code added this to the v0.2.0 milestone Jun 18, 2026
@tiana-code
tiana-code merged commit 1ad9c02 into main Jun 18, 2026
8 checks passed
@tiana-code
tiana-code deleted the feat/E-03-retry-dlq branch June 18, 2026 06:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

F-03.6 Retry topic + DLQ pattern

1 participant