Skip to main content

Spring Kafka 3 to 4 Migration Guide

Moving a Spring application from Spring Kafka 3 to 4 looks like a version bump. It is three changes at once: the Kafka client itself jumps to 4.x, Spring Boot 4 stops configuring Kafka unless you use a new starter, and the JSON serializer you probably use is built on a Jackson that Boot 4 no longer puts on your classpath. This article migrates one small app and shows what each of those does when it goes wrong. You need to know what a Kafka topic, a producer and a consumer are. Everything else is explained as it appears.
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 no KafkaTemplate 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 the spring-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 as localhost 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 (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 old JsonSerializer is a Jackson 2 class, so it works in tests and has nothing to run on in production.
To see it, compile and run the small serializer check with the runtime classpath only:
$ 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.

Where Jackson 2 is, after the Boot 4 starter is addedTest classpathJackson 2 + Jackson 3JsonSerializer works: tests passRuntime classpathJackson 3 onlyJsonSerializer: class file not found
The figure is the difference between the third and fourth transcripts: the same class is fine in one place and cannot even be compiled in the other. It is worth adding a check like this to your own build, because ordinary tests will not catch it.

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 on spring-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

BeforeAfterHow I know
spring-kafka dependencyspring-boot-starter-kafkamissing KafkaTemplate, then a passing test
Jackson 2 via spring-boot-starter-jsonspring-boot-starter-jackson (Jackson 3)dependency list, runtime probe
JsonSerializer, JsonDeserializerJacksonJsonSerializer, JacksonJsonDeserializerruntime probe, passing test
kafka-clients 3.9.2kafka-clients 4.2.1dependency list
embedded broker on 127.0.0.1embedded broker on localhosttest 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.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.