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. Spring Kafka uses manual acknowledgment semantics internally to support asynchronous completion and 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. Performs blocking retries for the affected partition (@RetryableTopictakes a different, non-blocking approach via separate retry topics).@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 request and reply via theKafkaHeaders.CORRELATION_IDheader (UUID by default, customisable through aCorrelationIdStrategy).- Transactions —
KafkaTransactionManager+ transactional producer. For "do DB write + publish" atomicity, the historical option wasChainedKafkaTransactionManager; that class is now deprecated, and current guidance favors the outbox pattern or Spring's transaction synchronization to coordinate the DB transaction and the producer transaction.
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) may process concurrently within a partition depending on listener configuration and async completion semantics — 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.
CPU / Memory / Network at Runtime
A Spring Kafka application is an ordinary Spring Boot process. Spring Kafka adds three runtime cost centres on top of whatever the application code itself does: listener-container threads driving polls, retry / DLT infrastructure holding state for in-flight failures, and the underlying Apache Kafka client connections to the brokers. Almost nothing else is allocated unless you opt in (transactions, retry topics, request-reply replies).
CPU / threading model
- Listener threads —
ConcurrentMessageListenerContainercreatesconcurrencychildKafkaMessageListenerContainers. Each child runs oneListenerConsumerthread that owns oneKafkaConsumerinstance and loopspoll → dispatch → commit. CPU on this thread is application-dependent: user listener code usually dominates, but heavy deserializers (Avro / Protobuf / Jackson JSON) can contribute meaningfully, and the Spring conversion toMessage<?>sits on top of that. - Synchronous vs async dispatch — with a
void/ blocking return type, the user method runs on the listener thread itself, so CPU and throughput are bound by what the user code does. With async return types (CompletableFuture,Mono, Kotlinsuspend), the listener thread hands off and returns topollimmediately; CPU moves to the scheduler the user picked. - Producer sender thread —
KafkaTemplatewraps a single Apache KafkaKafkaProducer, which runs its own internal sender thread for batching and flushing. Most apps do not see this thread directly; it is the producer's network I/O thread. - Retry-topic scheduler — when
@RetryableTopicis enabled, Spring Kafka starts a small scheduled-executor pool (RetryTopicSchedulerLifecycle) that wakes records out of their backoff bucket and republishes them. Idle most of the time; spikes only with retry traffic. - Background hooks — Micrometer observation timers, transaction synchronizations, error handlers (
DefaultErrorHandler) all run on the listener thread between polls. No extra threads spun up per record.
Memory layout
- Poll buffer per container — each
KafkaConsumercan hold up tomax.poll.records × max(record-size)bytes worth ofConsumerRecords between polls. The Spring conversion path (record →Message<?>) allocates per record; the GC pressure here is usually the dominant Spring Kafka cost, not heap retention. FailedRecordTracker— one map entry per in-flight failing(topic, partition, offset)tracking attempt count + next backoff. Bounded by how many distinct records are currently failing across all partitions of all listener containers — typically dozens, not thousands.- Retry topic state — when
@RetryableTopicgenerates the retry topic chain, the in-memory structures are small (one record-list per backoff bucket while waiting). The records themselves live in Kafka, not in heap. KafkaTemplate/ producer batches — the underlyingKafkaProducerkeeps aRecordAccumulatorsized bybuffer.memory(default 32 MB). A backed-up producer (slow broker) is the most common heap-growth surprise; it surfaces asRecordTooLargeExceptionorTimeoutExceptionrather than OOM, but the buffer is real.- Transactional state — with
transactional.idset, the producer maintains an in-flight transaction state object and one open producer per transactional ID; the consumer side holds a small offset map for the active transaction window. Negligible.
Network traffic
- Long-lived TCP per broker per client — each
ListenerConsumer'sKafkaConsumertypically maintains long-lived TCP connections to the brokers it actually communicates with (leaders of its assigned partitions plus the group coordinator), opened lazily on first use. Same shape for theKafkaTemplate's producer. Withconcurrency=Nthe consumer connection count is roughlyN × brokers_with_assigned_partitions. - Poll frequency — each listener thread sends a
FetchRequestroughly everyfetch.max.wait.ms(or sooner if data is available). Spring Kafka defaultsenable.auto.commit=false, so theOffsetCommitcadence is driven by the container'sAckMode(RECORD/BATCH/TIME/COUNT/COUNT_TIME/MANUAL/MANUAL_IMMEDIATE), not byauto.commit.interval.ms. Heartbeats to the group coordinator run on a separate background thread inside the consumer atheartbeat.interval.ms. - Retry / DLT republishes — every retry attempt or DLT publication is an extra
ProduceRequest. With@RetryableTopicand N retry levels, a failing record turns into N+1 produces (original failure → retry-1 → retry-2 → ... → DLT). Visible in producer-side metrics; usually small unless failure rates spike. - Request-reply (
sendAndReceive) — adds one outboundProduceRequeston the request topic + one inboundFetchRequeston the reply topic for every call. Spring Kafka correlates by header, so the reply consumer is shared. - Transactions — adds
InitProducerIdat startup andAddPartitionsToTxn/EndTxnround trips per transaction boundary. One transactional commit = a handful of extra small requests, not per record.
Four layers stacked. APP = Spring Kafka structures. JVM = the threads those structures run on plus the heap / off-heap buffers they allocate. OS = the cores, kernel RAM (page cache + TCP socket buffers) and NIC the JVM lives on top of. BROKERS sit across the NIC. The expensive runtime axis for Spring Kafka is user listener CPU on the sync path or scheduler choice on the async path; memory surprises usually come from a backed-up producer's RecordAccumulator; network is bounded by NIC + per-socket TCP buffer size, not by Spring Kafka itself.
Joint with Apache Kafka
The same Spring Kafka structures and the same Apache Kafka cluster drawn together, so the boundary between "client library" and "broker" is visible end-to-end. The cluster in the middle is exactly the broker shown on /apache-kafka.html; the producer and consumer flanks are the components covered on the rest of this page.
Joint architecture
Blue = produce / commit requests. Green = consumer fetch (zero-copy on the broker side). Dashed = leader-to-follower replication and the internal dispatch chain inside the listener container.
End-to-end sequence
A single record's journey across both halves — produce path on top, consume path on the bottom, with offset commit at the end.
Steps 1–7: produce path through KafkaTemplate → broker leader → ISR follower → HW advance → ack. Steps 8–13: consume path through ListenerConsumer → adapter → user method → offset commit to __consumer_offsets. The failure branch is detailed in the Message Flow section above.
Source Tree (for contributors)
Official source: github.com/spring-projects/spring-kafka. Build system is Gradle (./gradlew build); JDK 17+ for the 3.x line, JDK 17+ for 4.x. Contribution flow is the standard CONTRIBUTING.adoc — fork → branch off main (next major) or a X.Y.x release branch → PR. CI runs on GitHub Actions.
Solid arrow = version managed by; dashed arrow = depends on. A typical bugfix PR touches one file under spring-kafka/src/main/java/... and one test under spring-kafka/src/test/java/.... A new test utility goes to spring-kafka-test/; a release bumps spring-kafka-bom/.
Top-level modules
spring-kafka/— the main module. Everything inorg.springframework.kafka.*: listener containers,KafkaTemplate, error handlers, retry topics, transactions, support classes. Sources undersrc/main/java, unit tests undersrc/test/java(JUnit 5 + Mockito), integration tests undersrc/test/java/...with@EmbeddedKafka.spring-kafka-test/— test utilities published as a separate artifact. The single most-used class isEmbeddedKafkaBroker(programmatic in-process Kafka cluster);@EmbeddedKafkais its JUnit 5 hook. Also shipsKafkaTestUtilsfor record polling and offset assertions.spring-kafka-bom/— Bill of Materials. A singlepom.xml-equivalent that other Spring projects (Spring Boot, Spring Cloud Stream) import to lock the Spring Kafka + transitively-managed versions.spring-kafka-docs/— Antora-based reference documentation. Adoc sources are here; the rendered site lives atdocs.spring.io/spring-kafka/reference. Doc PRs touch this module only.samples/— runnable example projects covering listener concurrency, retry topics, transactions, Kafka Streams binding, etc. Used as living documentation and as regression fixtures for behaviour-change PRs.src/(top-level) — checkstyle config, copyright headers, Gradle convention scripts shared by the modules above.build.gradle/settings.gradle/gradle.properties— Gradle project setup. The version is ingradle.properties; bump there to cut a release.
Version history
- 2.x (2018 – 2022) — built on Spring Framework 5 / Spring Boot 2 / Java 8+. Introduced
@KafkaListener's concurrency model, transactionalKafkaTemplate, the original error-handler hierarchy, and the first retry-topic implementation. - 3.0 (Nov 2022) — Jakarta EE 9 / Spring Boot 3 / Java 17+ baseline. Package rename for
javax.*→jakarta.*. NewCommonErrorHandleras the single error-handler contract;DefaultErrorHandlerreplaced the olderSeekToCurrentErrorHandler/RecoveringBatchErrorHandler. - 3.1 – 3.3 (2023 – 2024) —
@RetryableTopichardening, observability via Micrometer, Kafka client 3.5 → 3.7 upgrades, KRaft test support inEmbeddedKafkaBroker. - 4.0 (Jun 2025) — Spring Framework 7 / Spring Boot 4 / Java 17+. Native compilation (GraalVM) support tightened.
spring-kafka-streamsbehaviour aligned with Apache Kafka 4.0 (KRaft-only). - 4.1 (latest at the time of writing) — async listener path overhaul (suspend /
Mono/CompletableFuturereturn-types funneled through a single async-failure callback), strict partition-ordered retry across async boundaries.
Branching & release model
main tracks the next major. Each minor (3.3.x, 4.0.x, 4.1.x) gets its own long-lived branch; in practice contributor PRs target the branch the maintainer asks for (often the actively maintained one), and a maintainer handles back/forward-porting across the supported branches when the diff doesn't apply cleanly. Release tagging convention is vX.Y.Z; milestones (X.Y.0-M1, X.Y.0-RC1) precede every .0.