Upgrading Spring Framework Version in Spring Boot
Spring Boot does not let you reference the Spring Framework directly in most projects — instead, it manages the version through its own dependency management (BOM). Every Spring Boot release is tied to a specific, tested Spring Framework version. Sometimes, though, you need a newer Spring Framework version before Spring Boot officially bumps it — for example, to pick up a security fix or a feature not yet available in your current Spring Boot release. This article explains how Spring Boot manages the Spring Framework version, how to check it, how to override it safely, and how to verify the change didn’t break anything.
1. Spring Framework Version Management in Spring Boot
Spring Boot uses a Bill of Materials (BOM) — spring-boot-dependencies — that pins versions for Spring Framework, Spring Data, Spring Security, Jackson, and dozens of other libraries. When you add spring-boot-starter-web without specifying a version, Maven/Gradle resolves the Spring Framework version from this BOM, not from Spring Framework’s own release line. This means: upgrading Spring Boot usually upgrades Spring Framework automatically. Overriding Spring Framework’s version independently is the exception, not the norm, and should be done cautiously since Spring Boot only tests against the version it ships with.
2. Checking the Current Spring Framework Version
You can check the resolved version a few ways:
2.1 Maven dependency tree
Before changing anything, confirm which Spring Framework version Maven has actually resolved for your build. The command below walks the full dependency tree and filters it down to just the spring-core artifact.
mvn dependency:tree | grep spring-core
mvn dependency:tree prints every dependency Maven resolved for the project, including transitive ones pulled in indirectly through spring-boot-starter-web and similar starters. Piping it through grep spring-core narrows the (often very long) output down to the one line showing spring-core, since every Spring module — spring-web, spring-aop, spring-context, etc. — is pinned to the same Spring Framework version, so spring-core‘s version is representative of the whole family. The version shown here is the one your build actually uses, which is what matters, since it may differ from what you expect if another dependency overrides it.
2.2 At runtime (programmatically)
Sometimes you want to confirm the version from inside a running application rather than from the build tool — for example, to expose it on a health or diagnostics endpoint. Spring Framework ships a small utility class for exactly this.
import org.springframework.core.SpringVersion;
public class VersionCheck {
public static void main(String[] args) {
System.out.println("Spring Framework version: " + SpringVersion.getVersion());
}
}
SpringVersion is a small class bundled with spring-core that reads version information embedded in the JAR’s manifest (Implementation-Version) at class-loading time. Calling SpringVersion.getVersion() returns that value as a String — here "6.1.13" — with no reflection or external calls needed. Because this reads the manifest of whichever spring-core-<version>.jar is actually on the classpath at runtime, it’s a reliable way to catch cases where the version resolved at build time (via mvn dependency:tree) differs from what’s packaged and running, which can happen with shaded JARs or classpath conflicts.
3. Upgrading the Current Spring Framework Version
Suppose your project uses Spring Boot 3.3.4 (which ships Spring Framework 6.1.13), and you need Spring Framework 6.1.14 for a specific fix, without yet moving to a newer Spring Boot version. You override the property that the Spring Boot BOM exposes for this purpose.
3.1 pom.xml (Maven)
The spring-boot-starter-parent POM defines a spring-framework.version property internally that drives every Spring module’s version. Declaring the same property in your own <properties> block overrides it without touching the parent or adding explicit version numbers to individual Spring dependencies.
<project>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
</parent>
<properties>
<!-- Overrides the Spring Framework version pinned by spring-boot-starter-parent -->
<spring-framework.version>6.1.14</spring-framework.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
The <parent> block pulls in spring-boot-starter-parent:3.3.4, which transitively imports the spring-boot-dependencies BOM — the place where spring-framework.version is originally set to 6.1.13. Maven resolves properties using a “nearest declaration wins” rule: since your project’s own <properties> block is closer to the root than the inherited parent POM, your value of 6.1.14 takes precedence everywhere that property is referenced. The <dependency> entry for spring-boot-starter-web is unchanged and still has no explicit version — it continues to resolve from the BOM, just now against the overridden property value, so spring-web, spring-core, spring-aop, etc. all pick up 6.1.14 automatically.
3.2 build.gradle (Gradle, equivalent override)
Gradle doesn’t use Maven properties, but the io.spring.dependency-management plugin reads the same BOM and exposes its properties through the ext extension, so the override mechanism is conceptually identical.
plugins {
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
}
ext['spring-framework.version'] = '6.1.14'
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
The org.springframework.boot plugin applies the spring-boot-dependencies BOM for version 3.3.4, and the io.spring.dependency-management plugin is what actually enforces those managed versions on your dependencies. Setting ext['spring-framework.version'] = '6.1.14' writes directly into Gradle’s extra-properties container, which the dependency-management plugin checks before falling back to the BOM’s own value — so it overrides 6.1.13 the same way the Maven <properties> override does. The implementation 'org.springframework.boot:spring-boot-starter-web' line still omits a version string, meaning it continues to resolve through managed dependencies, now at the overridden 6.1.14.
Make note that this technique only works for a minor patch bump within the same major/minor line Spring Boot already supports (e.g., 6.1.13 → 6.1.14). Jumping Spring Framework major/minor versions (e.g., 6.1 → 6.2) ahead of Spring Boot is unsupported and often breaks due to API changes.
4. Investigating Unexpected Dependency Versions
Overriding one property can cause Maven to silently resolve other transitive dependencies to mismatched versions (e.g., spring-aop staying old while spring-core is new). Always inspect the resolved tree after an override.
Narrow the full dependency tree down to just the Spring Framework artifacts so you can scan all of them at once instead of hunting through hundreds of unrelated lines.
mvn dependency:tree -Dincludes=org.springframework
The -Dincludes=org.springframework flag filters Maven’s dependency-tree output to only artifacts whose group ID matches org.springframework, which covers every core Spring Framework module (spring-web, spring-webmvc, spring-core, spring-aop, and so on) while excluding Spring Boot’s own starter artifacts and third-party libraries. This gives you a focused view of exactly what you care about after an override: whether all Spring Framework modules landed on the same resolved version.
Running the command above against the overridden project produces a tree like this:
Output: [INFO] com.example:demo:jar:0.0.1-SNAPSHOT [INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.3.4:compile [INFO] | +- org.springframework:spring-web:jar:6.1.14:compile [INFO] | +- org.springframework:spring-webmvc:jar:6.1.14:compile [INFO] | +- org.springframework:spring-core:jar:6.1.14:compile [INFO] | \- org.springframework:spring-aop:jar:6.1.14:compile
Each line shows groupId:artifactId:packaging:version:scope for a resolved dependency, indented to show which parent pulled it in. Here, every Spring module nested under spring-boot-starter-web — spring-web, spring-webmvc, spring-core, and spring-aop — shows 6.1.14, confirming the override from Section 3 applied consistently across the whole family. If even one of these showed the old 6.1.13, that would indicate another dependency is forcing its own version and overriding your BOM property.
If you spot an inconsistent version, find out who’s pulling in the old one:
mvn dependency:tree -Dverbose | grep -B 5 "spring-aop:jar:6.1.13"
The -Dverbose flag tells Maven to print every dependency path it considered, including ones it rejected in favor of another version — information that’s hidden in the default tree output. Piping that through grep -B 5 prints the five lines before each match of the old 6.1.13 version string, which typically shows the parent artifact and the conflict/omission marker Maven used when resolving that version, letting you trace exactly which dependency declared it explicitly instead of deferring to the BOM.
And confirm there are no duplicate/conflicting jars at runtime:
import org.springframework.aop.framework.AopProxy;
public class ClasspathCheck {
public static void main(String[] args) {
System.out.println(AopProxy.class.getProtectionDomain()
.getCodeSource().getLocation());
}
}
AopProxy.class.getProtectionDomain().getCodeSource().getLocation() asks the JVM where the class was actually loaded from on disk — in this case, which spring-aop JAR file ended up on the classpath. This matters because a build tool can resolve one version on paper while a different JAR (e.g., shaded into a fat JAR, or pulled in by an unmanaged transitive dependency) ends up being the one the JVM actually loads at runtime. Checking a representative class from each Spring module this way catches that class of bug directly, rather than inferring it from build logs.
Output: file:/Users/dev/.m2/repository/org/springframework/spring-aop/6.1.14/spring-aop-6.1.14.jar
The printed path points to spring-aop-6.1.14.jar in the local Maven repository, matching the version from the dependency tree in Section 4’s first example. This confirms the JAR actually loaded at runtime matches the version you intended, closing the loop between what the build declared and what the running application is using.
5. Verifying the Upgrade
After the override, verify the application still boots and behaves correctly:
- Run
mvn clean verify— ensure the full test suite passes. - Start the app and check the startup banner/logs for the Spring version.
- Hit a sample endpoint to confirm core features (serialization, AOP, transactions) still work.
The build and classpath checks from Section 4 only confirm which JARs are present — they don’t prove the running application actually works on the new version. Exposing the version through a real HTTP endpoint confirms the whole stack (embedded Tomcat, Spring MVC request mapping, and the overridden spring-core JAR) boots and serves requests correctly together.
@RestController
class VersionController {
@GetMapping("/version")
String version() {
return "Spring Framework: " + org.springframework.core.SpringVersion.getVersion();
}
}
@RestController registers this class as a Spring MVC bean whose methods return response bodies directly (rather than view names), and @GetMapping("/version") maps HTTP GET requests on /version to the version() method. Inside the method, the same SpringVersion.getVersion() call from Section 2 reads the version baked into spring-core‘s manifest, but this time from within a fully running Spring Boot application context rather than a standalone main method. If the override is wired correctly and the app starts without errors, hitting this endpoint proves both the dependency resolution and the Spring MVC request-handling pipeline work end-to-end on the new version.
Output (curl http://localhost:8080/version): Spring Framework: 6.1.14
Calling curl http://localhost:8080/version against the running application returns Spring Framework: 6.1.14, the exact string built by VersionController.version(). Seeing 6.1.14 here — matching both the dependency tree output and the classpath check from Section 4 — is the final confirmation that the override took effect consistently from build time, through the classpath, to a live, request-serving application.
6. Best Practices
| Practice | Why it matters |
|---|---|
| Prefer upgrading Spring Boot over overriding Spring Framework alone | Boot’s own version is tested as a cohesive unit; manual overrides aren’t |
| Only override for patch-level fixes | Minor/major jumps often break internal APIs Boot relies on |
Always check mvn dependency:tree after overriding | Catches silent version mismatches across Spring modules |
| Run full regression tests before and after | Confirms no behavioral breakage from the version bump |
| Track the override with a comment and ticket reference | Reminds the team to remove it once Boot catches up natively |
| Check Spring Framework release notes for breaking changes | Even patch releases occasionally include behavior changes |
7. Conclusion
Spring Boot’s dependency management makes version upgrades mostly automatic, but there are valid cases for overriding the Spring Framework version independently — typically to grab a critical fix ahead of the next Boot release. The safe path is: override only the spring-framework.version property for small patch bumps, inspect the resolved dependency tree for inconsistencies, and run your test suite to verify nothing broke. Once Spring Boot itself ships the newer version, remove the manual override to go back to the fully-tested, Boot-managed baseline.




