Two migrations get confused with each other. One is historical — classic Spring to Spring Boot, which most codebases finished years ago. The other is live and is probably why you are here: Spring Boot 3 to Spring Boot 4. This lesson covers both, briefly for the first and carefully for the second.
Part 1 — Classic Spring to Spring Boot
A pre-Boot Spring MVC application needed three things Boot deleted outright.
The XML
Component scanning, the view resolver, the datasource and the transaction manager were declared in XML:
<beans xmlns="http://www.springframework.org/schema/beans">
<context:component-scan base-package="com.example.app"/>
<mvc:annotation-driven/>
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/app"/>
<property name="username" value="root"/>
<property name="password" value=""/>
</bean>
<bean class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
</beans>All of that becomes one annotation and four properties:
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}spring.datasource.url=jdbc:mysql://localhost:3306/app
spring.datasource.username=root
spring.datasource.password=The WAR and the servlet container
You built a WAR, installed Tomcat, edited server.xml, dropped the WAR in
webapps/ and restarted. Boot produces an executable jar with the server inside it, so
deployment is java -jar. Lesson 2 covers this.
The version matrix
You picked a Spring version, then found a Hibernate version compatible with it, then a Jackson version compatible with both. The Boot parent does this for you.
Migration order that works: add the Boot parent and starters; add a
@SpringBootApplication class; delete XML one file at a time, checking the app still
starts after each; move web.xml settings into application.properties;
switch packaging from war to jar last, because it is the step that is
awkward to undo.
Part 2 — Spring Boot 3 to Spring Boot 4
This is a bigger break than 2 to 3 was in some ways, because the changes are spread across renamed artifacts and moved packages rather than concentrated in one namespace rename.
Java 21 is the floor
Boot 3 ran on Java 17. Boot 4 requires Java 21. This is not negotiable and it is the first thing to check.
⚠️ Artifacts were renamed, and the old names are simply gone
The one that catches everyone:
<!-- Boot 3 -->
<artifactId>spring-boot-starter-aop</artifactId>
<!-- Boot 4 -->
<artifactId>spring-boot-starter-aspectj</artifactId>The old name is not deprecated — it is absent from the BOM. So the build fails with:
[ERROR] 'dependencies.dependency.version' for
org.springframework.boot:spring-boot-starter-aop:jar is missing.Nothing in that message mentions a rename. It reads like a missing
<version> tag, and the natural next move — pinning a version by hand — makes it
worse. spring-boot-starter-web similarly becomes
spring-boot-starter-webmvc.
Not every starter changed, which is the trap.
spring-boot-starter-cache, spring-boot-starter-mail and
spring-boot-starter-thymeleaf all kept their names. You cannot apply a rule; you have to
check each one against the BOM.
⚠️ Autoconfiguration classes moved package
Boot 4 split autoconfiguration into per-technology modules, and the classes moved with them:
// Boot 3
import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer;
// Boot 4
import org.springframework.boot.jms.autoconfigure.DefaultJmsListenerContainerFactoryConfigurer;Note the shape of the change: autoconfigure.jms became jms.autoconfigure.
The technology name moved in front. That pattern holds across the other modules, so once you have seen
it the rest are mechanical.
⚠️ Depending on a library no longer implies its autoconfiguration
Covered in lesson 2, and repeated here because it is a migration issue specifically. In Boot 3,
liquibase-core was enough. In Boot 4 it gives you the library and no autoconfiguration,
so Liquibase never runs — and the error you get is from Hibernate, several layers away:
Schema-validation: missing table [product_size]The fix is the starter, not the library:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-liquibase</artifactId>
</dependency>Spring Framework 7 comes with it
Boot 4 brings Framework 7, which has its own changes.
Retry moved into the core container. The separate spring-retry
library is now legacy:
// Boot 3 + spring-retry
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.annotation.Retryable;
// Boot 4 / Framework 7 — no extra dependency
import org.springframework.resilience.annotation.EnableResilientMethods;
import org.springframework.resilience.annotation.Retryable;They are not drop-in equivalents. The built-in version has no @Recover
— it rethrows once retries are exhausted, and you handle the failure at the call site. Lesson 29
covers both.
Jackson 2 types are deprecated for removal. Anything named
MappingJackson2* has a Jackson 3 replacement:
// Deprecated in Framework 7
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
// Replacement
import org.springframework.jms.support.converter.JacksonJsonMessageConverter;Spring Security 7 removed the old chained-setter DSL. Configuration written against Security 5 will not compile. Lesson 24 covers the lambda DSL that replaced it.
The Gradle plugin needs a newer Gradle
Boot 4.1's Gradle plugin requires Gradle 8.14 or later, or 9.x:
Spring Boot plugin requires Gradle 8.x (8.14 or later) or 9.x.
The current version is Gradle 8.12That message at least names the problem. The fix is a committed wrapper, so the build does not
depend on whichever Gradle is on someone's PATH — lesson 33.
A migration order that works
- Java 21 first. Build and test on 21 while still on Boot 3, so the JDK upgrade and the framework upgrade fail separately.
- Bump the parent to 4.1.0 and fix the pom until it resolves. Expect renamed artifacts; check each against the BOM instead of guessing.
- Fix compilation — moved packages and the Framework 7 replacements above.
- Start the app with
--debugand read the condition evaluation report. This is where silently-missing autoconfiguration shows up, and it is much cheaper to find here than in production. - Run the tests. Security and slice tests are where behavioural changes surface.
Step 4 is the one people skip and the one that pays. A missing autoconfiguration does not fail the build and does not fail startup — it fails later, somewhere unrelated, exactly as the Liquibase case above did.
Next: structuring a project — the package layout the rest of this track uses, and the one structural mistake that is genuinely hard to diagnose.