Spring Boot – Migrating from Spring, and from Boot 3 to 4

June 15, 20264 min readUpdated 8/18/2026

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.12

That 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

  1. Java 21 first. Build and test on 21 while still on Boot 3, so the JDK upgrade and the framework upgrade fail separately.
  2. 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.
  3. Fix compilation — moved packages and the Framework 7 replacements above.
  4. Start the app with --debug and read the condition evaluation report. This is where silently-missing autoconfiguration shows up, and it is much cheaper to find here than in production.
  5. 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.