Versions tested. Before: Spring Boot 3.5.16, Spring Kafka 3.3.16, kafka-clients 3.9.2, JDK 21. After: Spring Boot 4.1.1, Spring Kafka 4.1.1 (latest GA), kafka-clients 4.2.1, JDK 25.0.4.1. Spring Kafka 4.2.0-M1 and M2 are milestones and were not tested. Date: 28 September 2026. Companion code: spring-messaging-demo/kafka4-migration.
The starting point: a small Kafka app that works
The app has one event type, one@KafkaListener that stores what it receives, and one test that starts an embedded Kafka broker inside the test JVM, sends a message, and waits for the listener. Embedded means no Docker and no separate broker. Serialisation is JSON, configured only in properties:
spring.kafka.consumer.group-id=orders-group
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
spring.kafka.consumer.properties.spring.json.trusted.packages=com.ankurm.kafka
spring.kafka.consumer.properties.spring.json.value.default.type=com.ankurm.kafka.OrderEvent
Source: application.properties.
@SpringBootTest(properties = "spring.kafka.bootstrap-servers=${spring.embedded.kafka.brokers}")
@EmbeddedKafka(topics = "orders", partitions = 1)
class OrderFlowTest {
@Autowired KafkaTemplate<String, OrderEvent> template;
@Autowired OrderListener listener;
@Autowired EmbeddedKafkaBroker broker;
@Test
void sendAndReceive() {
template.send("orders", "k1", new OrderEvent("o-1", 3));
await().untilAsserted(() -> assertThat(listener.received).hasSize(1));
System.out.println("RESULT " + listener.received.get(0));
System.out.println("BROKERS " + broker.getBrokersAsString().replaceAll(":\\d+", ":PORT"));
}
}
Source: OrderFlowTest.java, lines 13–27.
$ mvn test # Boot 3.5.16, JDK 21
RESULT key=k1 id=o-1 qty=3
BROKERS 127.0.0.1:PORT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.853 s -- in com.ankurm.kafka.OrderFlowTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
org.apache.kafka:kafka-clients:jar:3.9.2:compile
org.springframework.kafka:spring-kafka:jar:3.3.16:compile
$ java -cp target/classes:<runtime classpath> SerdeProbe
SERIALIZED {"id":"o-1","qty":3}
Captured in 01-legacy-kafka3.txt.
The last lines show the same JSON serializer used on its own, with nothing but the application’s runtime dependencies on the classpath. That small check matters later, so remember it works here.Step 1: change only the Boot version
$ mvn test # only the Boot version changed to 4.1.1
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 5.728 s <<< FAILURE! -- in com.ankurm.kafka.OrderFlowTest
[ERROR] OrderFlowTest.sendAndReceive ? UnsatisfiedDependency Error creating bean with name 'com.ankurm.kafka.OrderFlowTest': Unsatisfied dependency expressed through field 'template': No qualifying bean of type 'org.springframework.kafka.core.KafkaTemplate<java.lang.String, com.ankurm.kafka.OrderEvent>' available: expected at
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0
[INFO] BUILD FAILURE
Captured in 02-boot4-no-starter.txt.
The build compiles and the test fails: there is noKafkaTemplate bean. Nothing in the source was wrong. In Boot 4 the Kafka auto-configuration no longer comes with the plain spring-kafka library. It lives in a Boot module that the new spring-boot-starter-kafka pulls in.
Step 2: switch to the Boot 4 Kafka starter
Replace thespring-kafka dependency by spring-boot-starter-kafka and change nothing else:
$ mvn test # spring-kafka replaced by spring-boot-starter-kafka; nothing else changed
RESULT key=k1 id=o-1 qty=3
BROKERS localhost:PORT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.853 s -- in com.ankurm.kafka.OrderFlowTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
org.apache.kafka:kafka-clients:jar:4.2.1:compile
org.springframework.boot:spring-boot-kafka:jar:4.1.1:compile
org.springframework.kafka:spring-kafka:jar:4.1.1:compile
$ mvn dependency:list | grep jackson # runtime scope, then test scope
runtime: tools.jackson.core:jackson-core:jar:3.1.5:compile
runtime: tools.jackson.core:jackson-databind:jar:3.1.5:compile
test-only: com.fasterxml.jackson.core:jackson-databind:jar:2.21.5:test
Captured in 03-starter-added-tests-pass.txt.
The test passes. Two things changed underneath it and both are visible in the transcript. The Kafka client went from 3.9.2 to 4.2.1, and the embedded broker now reports its address aslocalhost instead of 127.0.0.1; I only saw that in this output and did not investigate whether it matters for your tests.
A passing test that hides a production failure. Look at the last three lines of the transcript. The application’s runtime scope has Jackson 3 (To see it, compile and run the small serializer check with the runtime classpath only:tools.jackson) only. Jackson 2 (com.fasterxml.jackson) is on the classpath in test scope only (I did not trace which test dependency brings it). The oldJsonSerializeris a Jackson 2 class, so it works in tests and has nothing to run on in production.
$ compile and run SerdeProbe on the runtime classpath only (JsonSerializer from Spring Kafka 4.1.1)
[ERROR] class file for com.fasterxml.jackson.core.type.TypeReference not found
[ERROR] s2/src/main/java/com/ankurm/kafka/SerdeProbe.java:[9,45] cannot access com.fasterxml.jackson.core.type.TypeReference
Captured in 04-runtime-classpath-has-no-jackson2.txt.
Step 3: move to the Jackson 3 serializers
Spring Kafka 4.1.1 ships a parallel set of serializer classes built on Jackson 3, next to the old ones:$ jar tf spring-kafka-4.1.1.jar | grep support/serializer/(Json|Jackson)
JacksonJsonDeserializer.class JacksonJsonSerde.class JacksonJsonSerializer.class JacksonJsonTypeResolver.class JsonDeserializer.class JsonSerde.class JsonSerializer.class JsonTypeResolver.class
Captured in 06-serializer-classes.txt.
The migration is renaming the two classes in the properties and depending onspring-boot-starter-jackson so Jackson 3 is a declared dependency of the application rather than an accident of transitive dependencies. The spring.json.* property names stayed the same in my run:
spring.kafka.consumer.group-id=orders-group
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JacksonJsonSerializer
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JacksonJsonDeserializer
spring.kafka.consumer.properties.spring.json.trusted.packages=com.ankurm.kafka
spring.kafka.consumer.properties.spring.json.value.default.type=com.ankurm.kafka.OrderEvent
Source: application.properties.
$ mvn test # final migrated project (JacksonJsonSerializer, spring-boot-starter-jackson)
RESULT key=k1 id=o-1 qty=3
BROKERS localhost:PORT
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 8.478 s -- in com.ankurm.kafka.OrderFlowTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
org.apache.kafka:kafka-clients:jar:4.2.1:compile
org.springframework.boot:spring-boot-kafka:jar:4.1.1:compile
org.springframework.kafka:spring-kafka:jar:4.1.1:compile
$ java -cp target/classes:<runtime classpath> SerdeProbe
SERIALIZED {"id":"o-1","qty":3}
Captured in 05-migrated.txt.
Going deeper: what else may need attention
This app has no custom error handler, no retry topics, no transactions and no Kafka Streams, so I did not test how those behave across the version change. For error handling on Spring Kafka 4.1 see the existing article on this site.
The checklist
| Before | After | How I know |
|---|---|---|
spring-kafka dependency | spring-boot-starter-kafka | missing KafkaTemplate, then a passing test |
Jackson 2 via spring-boot-starter-json | spring-boot-starter-jackson (Jackson 3) | dependency list, runtime probe |
JsonSerializer, JsonDeserializer | JacksonJsonSerializer, JacksonJsonDeserializer | runtime probe, passing test |
| kafka-clients 3.9.2 | kafka-clients 4.2.1 | dependency list |
embedded broker on 127.0.0.1 | embedded broker on localhost | test output |
Should you migrate now?
Yes, with one extra check. Spring Kafka 4.1.1 is GA and this small app migrated cleanly once the three dependency changes were made. Add a runtime-classpath check for your serializers, since the tests can pass without it. Anything beyond what I ran — retry topics, transactions, Kafka Streams, security settings — needs its own test, and 4.2 is still in milestones.
No Comments yet!