Walk through configuring @RetryableTopic: attempts, backoff topics, exception classification, and where the DLT fits.
answer
- attempts (default 3) + @Backoff(delay,multiplier)
- Auto topics: -retry-0/-retry-1/.../-dlt
- include/exclude = exception classifier
- @DltHandler method handles exhausted records
- RetryTopicConfiguration bean = global policy
basics
~20 s@RetryableTopic on a listener tells Spring to auto-create retry topics with increasing delays and a final DLT. You set attempts, the backoff (delay/multiplier), which exceptions to include/exclude, and a @DltHandler method to process records that exhausted all retries.
solid answer
~40 s@RetryableTopic (on the @KafkaListener method or class) enables non-blocking retries. Key attributes: attempts (total tries, default 3), backoff = @Backoff(delay, multiplier, maxDelay) for fixed or exponential spacing, and topicSuffixingStrategy (suffix with index vs. delay value). You control classification with include/exclude exception arrays (or includeNames/excludeNames) and retryTopicSuffix/dltTopicSuffix for naming. autoCreateTopics governs topic creation; numPartitions/replicationFactor set their layout. With MULTIPLE_TOPICS strategy (default) Spring creates one topic per retry level (-retry-0, -retry-1, …) plus a DLT (-dlt). After the last retry fails, the record lands in the DLT, where a method annotated @DltHandler in the same class processes it (log, alert, or store). You configure global defaults via a RetryTopicConfiguration bean instead of the annotation when you want it applied broadly.
go deeper
Know the annotation creates retry topics and a DLT, with attempts and a backoff delay you configure.
Configure attempts, exponential @Backoff, include/exclude classification, suffixing strategy, and a @DltHandler.
Choose multiple-vs-single-topic strategy, global RetryTopicConfiguration, dltStrategy, and reason about replay/headers.
Standardize retry-topic naming, partitioning, and DLT replay tooling across teams; govern storage and observability impact.
## Enabling it Add `@EnableKafkaRetryTopic` (or historically `@EnableKafka` plus a `RetryTopicConfiguration` bean) to your config, then annotate a listener method: ``` @RetryableTopic(attempts = "4", backoff = @Backoff(delay = 1000, multiplier = 2.0)) @KafkaListener(topics = "orders") public void handle(Order o) { ... } ``` ## The attributes that matter - **`attempts`**: total processing attempts including the first (default `"3"`). 4 attempts = 1 main + 3 retries. - **`backoff`** = `@Backoff(delay, multiplier, maxDelay, random)`: with `multiplier > 1` you get **exponential** delays (1s, 2s, 4s…), capped at `maxDelay`; `random=true` adds jitter. Default is a fixed 1s. - **`topicSuffixingStrategy`**: `SUFFIX_WITH_INDEX_VALUE` (`-retry-0`, `-retry-1`) or `SUFFIX_WITH_DELAY_VALUE` (`-retry-1000`, `-retry-2000`). The latter lets multiple attempts share a topic by delay. - **`fixedDelayTopicStrategy`** / `SameIntervalTopicReuseStrategy`: whether equal-delay retries reuse a single topic. - **`include` / `exclude`** (and `includeNames`/`excludeNames`, `traversingCauses`): the **exception classifier**. `include` = only these exceptions are retried (everything else → DLT immediately); `exclude` = these go straight to DLT, the rest retry. `traversingCauses` lets the classifier look at wrapped causes. - **`dltStrategy`**: `FAIL_ON_ERROR` (default), `ALWAYS_RETRY_ON_ERROR`, or `NO_DLT` (no dead-letter topic at all). - **`autoCreateTopics`**, `numPartitions`, `replicationFactor`, `kafkaTemplate`, `listenerContainerFactory`. ## The topic chain With the default **multiple-topics** strategy, Spring provisions: `orders` → `orders-retry-0` → `orders-retry-1` → … → `orders-dlt`. Each retry topic has its own consumer; a record arriving before its delay has elapsed causes the container to **pause** until the timestamp + backoff is reached, then reprocess. A failure at level *n* republishes to level *n+1*; after the last retry the record goes to the **DLT**. ## Handling the DLT A method annotated **`@DltHandler`** in the same listener class receives records that exhausted retries: ``` @DltHandler public void dlt(Order o, @Header(KafkaHeaders.ORIGINAL_TOPIC) String topic) { log.error("Sent to DLT from {}", topic); } ``` If you don't define one, the default DLT behavior just logs. ## Global vs per-listener config For consistent policy across many listeners, define a single **`RetryTopicConfiguration`** `@Bean` (built via `RetryTopicConfigurationBuilder`) instead of repeating the annotation. The annotation wins for one-off tuning. ## Edge cases - The original record's **headers** carry metadata (`KafkaHeaders.ORIGINAL_TOPIC`, `EXCEPTION_FQCN`, `EXCEPTION_MESSAGE`, attempt count) so the DLT consumer can diagnose failures. - Mixing blocking + non-blocking: `DefaultErrorHandler` can still apply quick blocking retries before the record is forwarded to the retry-topic chain. - If `attempts="1"`, there are no retry topics — a failure goes directly to the DLT.
- How do you make some exceptions skip retries and go straight to the DLT?Use exclude (or excludeNames) to list non-retryable exceptions, or use include to allow-list only the retryable ones — anything not included routes directly to the DLT. traversingCauses lets the classifier inspect wrapped causes.
- What headers are available on a record that reaches the DLT?Spring adds KafkaHeaders like ORIGINAL_TOPIC, ORIGINAL_PARTITION, ORIGINAL_OFFSET, ORIGINAL_TIMESTAMP, plus EXCEPTION_FQCN, EXCEPTION_MESSAGE, EXCEPTION_STACKTRACE, and the attempt count — enough to diagnose and optionally replay.
saying these in an interview costs you the question
- Saying @DltHandler is required — without it the DLT just logs; the handler is optional.
- Confusing include/exclude semantics — include is an allow-list (only-these-retry), exclude is a deny-list.
- Thinking attempts counts only retries — it includes the initial attempt (attempts=3 → 1 main + 2 retries).
- Assuming exponential backoff is default — default @Backoff is a fixed 1s; you need multiplier>1 for exponential.