Reference · Spring Ecosystem
Spring Kafka — Architecture, Message Flow, Features, Internals
A compact reference to Spring Kafka: the container hierarchy that drives @KafkaListener, the record dispatch and error-handling flow, the features that come up day-to-day (DefaultErrorHandler, @RetryableTopic, DLT, transactions, async return types, Kotlin coroutines), and the source files where the real logic lives.
1. Architecture
Spring Kafka layers a Spring-managed container hierarchy on top of the plain Kafka client. The application registers a KafkaListenerContainerFactory bean; the framework discovers @KafkaListener-annotated methods and asks the factory to produce a ConcurrentMessageListenerContainer per listener. That container spins up one KafkaMessageListenerContainer per concurrency unit, each of which runs a single-threaded ListenerConsumer that owns one Consumer client. ListenerConsumer drives the poll loop, hands records to a listener adapter chain, and routes failures to the configured CommonErrorHandler (most often DefaultErrorHandler with a backoff and a recoverer such as DeadLetterPublishingRecoverer).
Blue arrows: normal dispatch and produce paths. Red arrows: failure path through CommonErrorHandler to the recoverer. Each KafkaMessageListenerContainer is single-threaded — concurrency above one is achieved by running more containers, not by parallelizing inside one consumer.
2. Message Flow
The path of a single record from broker poll to user method, with both the success path (offset commit) and the failure path (backoff retry → recover to DLT) drawn in.
Top half (1–7a): success path with offset commit. Bottom half (5b–11b): failure path through DefaultErrorHandler, with SeekUtils driving in-place retry until FailedRecordTracker's attempt budget is exhausted, then the recoverer publishes to a DLT and the offset commits past the bad record.
3. Features
Listener side
@KafkaListeneron a method — declarative subscription. Supports topic literals, patterns, explicit partition assignments, and SpEL.- Return types —
void, a value (used with@SendTofor request-reply),CompletableFuture<T>,Mono<T>, and Kotlinsuspend. Async return types automatically switchAckModetoMANUALwith out-of-order commits. - Batch mode — set the container factory's
batchListener=trueand acceptList<ConsumerRecord<K,V>>; the listener processes a full poll's worth of records per invocation. @KafkaHandleron class-level@KafkaListener— payload-type dispatch (different methods for different message payload classes).- Consumer-aware injection —
Consumer,Acknowledgment,ConsumerRecordMetadatacan be method parameters.
Error handling and retry
DefaultErrorHandler— the standardCommonErrorHandler. Uses aBackOff(FixedBackOff,ExponentialBackOff) for in-place retry and aConsumerRecordRecovererwhen attempts are exhausted. Blocks the partition during retry.@RetryableTopic— non-blocking retry. Failed records are published to a chain of retry topics (each with its own delay) and finally a DLT; the main partition keeps advancing. Useful when one bad record must not block its partition peers.DeadLetterPublishingRecoverer— pluggable recoverer that publishes the failed record to a DLT with structured headers (cause class, stack trace, original topic / partition / offset, timestamp).KafkaListenerErrorHandler— per-listener hook for translating exceptions before they reach the container'sCommonErrorHandler.- Reply-side error handling — when using
@SendTo, exceptions thrown by the listener can be converted to error replies viaKafkaTemplate'ssetBinaryReplyHeaderwiring.
Producer side
KafkaTemplate— main producer entrypoint. Supportssend(ProducerRecord),send(topic, key, value), transactionalexecuteInTransaction, andsendAndReceivefor request-reply.ReplyingKafkaTemplate— request-reply on top ofKafkaTemplate+ a reply container; correlates by a generated header.- Transactions —
KafkaTransactionManager+ transactional producer. Combine withJpaTransactionManagerviaChainedKafkaTransactionManagerfor "do DB write + publish" atomicity within the same Spring transaction boundary.
Exactly-once and ordering
- Exactly-once semantics — set
enable.idempotence=true+transactional.idon the producer, use transactionalKafkaTemplate, and set the consumer'sisolation.level=read_committed. - Per-key ordering — Kafka guarantees ordering inside a partition. Spring Kafka preserves it on the blocking
@KafkaListenerpath. Async return types (suspend /Mono/CompletableFuture) dispatch concurrently within a partition unless you wait for completion — consider this when ordering matters.
Operability
- Observation —
MessagingObservationconventions emit traces and metrics through Micrometer (producer, consumer, listener invocation spans). - Container lifecycle —
start()/stop()/pause()/resume()at runtime; per-partition pause viaConsumerSeekAwarecallbacks. ConsumerSeekAware— let the listener bean register seek callbacks to reposition partitions at startup / on demand (replay windows, offset reset on deploy).- Spring Boot auto-configuration —
spring.kafka.*properties drive producer / consumer / listener / admin / streams. Listener container factory is auto-namedkafkaListenerContainerFactory.
4. Internals — key source files
Where the logic actually lives. Browsing these in this order maps cleanly onto the dispatch flow above.
KafkaMessageListenerContainer$ListenerConsumer— the heart of the consumer side. Single-threaded poll loop. Owns theConsumer, thefailedRecordsdeque (for async failures), the offset tracking maps (offsetsInThisBatch,deferredOffsets,lastCommits), and the calls into the listener adapter andCommonErrorHandler.handleAsyncFailure()drainsfailedRecordsinto the error handler each loop iteration.ConcurrentMessageListenerContainer— supervisor that creates and lifecycles NKafkaMessageListenerContainerinstances based onconcurrency.MessagingMessageListenerAdapter/RecordMessagingMessageListenerAdapter— the listener-adapter chain. ConvertsConsumerRecordto a SpringMessage<?>, invokes the user method throughInvocableHandlerMethod, handles the return value (@SendToreply, async unwrap), and routes failures to either a sync throw or thecallbackForAsyncFailurehook that pushes intoListenerConsumer.failedRecords.DefaultErrorHandler(extendsFailedBatchProcessor) — the standardCommonErrorHandler. Coordinates theBackOff, callsSeekUtilsto register seeks, decides recover-vs-retry viaFailedRecordTracker, and throwsRecordInRetryExceptionto signal "this record is in retry, do not commit past it".FailedRecordTracker— keyed by(topic, partition, offset). Tracks attempt count for each in-flight failure, returns the next backoff interval, and decidesSTOP(recover) vsCONTINUE(retry).SeekUtils— utility that issues per-partitionconsumer.seek(...)calls to reposition the consumer back to the failing offset for the next poll-induced re-delivery.DeadLetterPublishingRecoverer— implementsConsumerRecordRecoverer. Builds the DLTProducerRecordwith structured headers (original topic / partition / offset, cause class, cause message, stack trace) and publishes via an injectedKafkaTemplate.KafkaBackoffAwareMessageListenerAdapter/RetryTopicConfigurer/RetryTopicConfiguration— the@RetryableTopicinfrastructure. Generates the retry topic chain at startup, wires a backoff-aware adapter that delays a record by the appropriate amount before invoking the user method on the next attempt.KafkaTemplate— producer-side entrypoint. Wraps aProducer, supports transactions throughProducerFactoryUtils, exposessend(...)/sendAndReceive(...)/executeInTransaction(...).ConsumerFactory/ProducerFactory— pluggable client factories.DefaultKafkaConsumerFactory/DefaultKafkaProducerFactoryare the standard implementations; both are aware of per-instance config overrides.KafkaListenerAnnotationBeanPostProcessor— discovers@KafkaListenermethods at startup and asks the chosenKafkaListenerContainerFactoryto create the matching container.