Mjukvare

Introduction to Spring Boot 4

NM

Nicklas Millard

published September 20, 2026

This introduction is not a how-to-write spring apps, but rather a primer to Spring and its facets, how to think about Spring, Spring framework, and Spring Boot.

Spring's own documentation acknowledges that Spring means different things. The word Spring is overloaded – I'll use it to refer to the entire collection of projects.

Before we dig into Spring Boot, we first need to know a little more about Spring Framework and be aware of the differences between Spring Framework and Spring Boot.

The Spring Framework itself focuses on providing core support for dependency injection, transaction management, web apps, data access, messaging, validation, testing, and then some. Framework 7 builds upon a selected set of Jakarta EE 11 specifications, such as Servlets, JPA, and bean validation.

A collection of libraries makes up Spring Framework, among them are:

LibraryNotes
spring-coreFoundational utilities, resource abstraction, type conversion, Environment
spring-beansBeanFactory, bean definitions, the DI machinery. Honours Jakarta Inject, so @Inject works in place of @Autowired
spring-contextApplicationContext, the stereotypes (@Component, @Service, @Repository), events, scheduling, caching. Integrates Jakarta Validation and Annotations
spring-expressionSpEL, that's the #{...} notation you'll see in e.g., @Query, etc.
spring-aopProxies and advisors, what makes @Transactional and @Cacheable work.
spring-txThe transaction abstraction and the DataAccessException hierarchy. Adapts Jakarta Transactions
spring-ormEntityManagerFactory bootstrapping and JpaTransactionManager. Bootstraps Jakarta Persistence providers
spring-webWeb infrastructure shared with WebFlux. Hooks into Jakarta Servlet, Validation, and XML Binding
spring-webmvcThe DispatcherServlet and annotated controllers. Sits directly on Jakarta Servlet
spring-jmsJmsTemplate and message listener containers. Wraps Jakarta Messaging
spring-websocketWebSocket endpoints and STOMP messaging. Layers over Jakarta WebSocket

These are just some of the framework libraries – there are many more, and they sit in a single repository. We'll cover the Jakarta Specifications vs Implementations in a later section. For now, it's just worth knowing that Spring writes against those interfaces and doesn't provide the implementations – in a Spring Boot app a starter usually picks the implementations for you.

Then there are Spring Projects. These projects add functionality to the Spring Framework, adding a programming model for a domain, such as data access with spring-data-jpa.

Boot, on the other hand, gives you configuration and packaging. It adds almost nothing to the programming model. Boot packages typically contain Auto-configurations – which is where the "Spring magic" reputation comes from (more on that later).

What about starters then? Starter projects, such as spring-boot-starter-data-jpa are nothing more than a single pom that defines imports. Essentially an empty jar with a list of dependencies – so you don't have to worry about what goes together.

Simply put, we can visualize it like so, knowing each level can reference any level below it:

Spring layers

Boot is an opinion. It's not a framework.

Coming from other languages and ecosystems, this might be a strange take on approaching software development, but the Java ecosystem has a culture of defining specifications and then let vendors provide competing implementations.

This is where Boot enters the picture. Boot is opinionated by choice. Boot says "these implementations of these versions are what goes together".

The specification is typically nothing more–yet enormously important–than a set of interfaces, annotations, and exceptions. Take a look at the Jakarta persistence API (JPA) repo.

Then there's the Hibernate implementation of JPA. EntityManager comes from the JPA specification, and Hibernate creates a Session interface which extends EntityManager. The actual implementation is SessionImpl.

So, for Spring Boot, being opinionated as it is, says if you want Spring Boot Data JPA version 4.1.1, then you also get Hibernate 7.4.5.Final.

Throw spring-boot-starter-webmvc into the mix, and now you're also getting packaging. The webmvc starter package decided Tomcat as the Servlet container implementation, and now you have an embedded web server. Coming from .NET, this is similar to how ASP.NET Core provides a Kestrel server.

See the list of Spring Boot platform dependencies.

The ApplicationContext is a key player in Spring.

All Spring Boot applications have this line in the main method. The SpringApplication.run is from Boot, but its return value is a ConfigurableApplicationContext which sits in the Framework.

// You typically don't assign the return to a variable - but this
// demonstrates that an ApplicationContext is returned
ConfigurableApplicationContext context = SpringApplication.run(MainApplication.class, args);

In this section we'll call the ApplicationContext "context" for short.

Since Spring is built on the principles of Inversion of Control (IoC) and Dependency Injection (DI), you'll need to know these design patterns intimately, otherwise you'll have a hard time understanding Spring, and I guarantee you'll be ineffective at using Spring.

In Java, and Spring even more so, you'll hear and read about "beans". A bean is an object the container creates and manages for you. In non-spring terms, you'd call it a service registered with the Dependency Injection Container. The context holds a bean definition for every bean in the application, and by default creates all of them at startup.

The important thing to know is, an instantiated bean is not something special – it's a regular object. The DI container just happens to manage it for you.

Loading application properties into the environment.

Loading properties is a separate job for Boot. Before the context exists, Boot gathers the application properties from files, environment variables, command-line arguments, etc., and loads them into a ConfigurableEnvironment instance. The environment is part of Spring Framework and not Boot. The environment instance is then passed on to the context down the line.

The environment is worth a brief look, because it isn't a single bag of key-value pairs. It's an ordered list of property sources:

ConfigurableEnvironment env = context.getEnvironment();
env.getPropertySources().forEach(ps -> System.out.println(ps.getName()));

For a web application that prints roughly this:

commandLineArgs
servletConfigInitParams
servletContextInitParams
systemProperties
systemEnvironment
random
Config resource 'class path resource [application.yml]' via location 'optional:classpath:/'

The order is the precedence – the first source that has the key wins. Your application.yml sits near the bottom, which is why starting the application with --some.property=value overrides the file without touching it. This is also the answer to "why is my property not being picked up?" – the key is usually there, but something further up the list is picked up first. This might be a bit counterintuitive to how we often think about orders, thinking that the last should win.

Loading configurations and registering bean definitions with the context.

The go-to approach for defining beans is annotations rather than XML, and it comes in two flavours: annotating a class with a stereotype such as @Component or @Service, or writing a @Configuration class with @Bean methods. We'll only focus on the modern, annotation approach here.

Now, this is a simple example, and you've likely created configuration classes before, but it serves as a starting point.

@Configuration
public class MyConfiguration {

    @Bean
    public MyService myService() {
        return new MyConcreteService();
    }
}

MyConfiguration declares a bean named myService of type MyService. The important part is that Spring doesn't call myService() straight away. All of this happens inside the context's refresh() method, which SpringApplication.run() calls for you, and it happens in two phases.

First, the context collects definitions. Boot registers a single class with the context: your main application class. When the context refreshes, a post-processor reads that class, follows @ComponentScan to find your components and configuration classes, and records a bean definition for each of them and for each @Bean method. Auto-configuration is handled last, once your own definitions are in place. At this point none of your beans exist. Think of it as the context holds blueprints to the objects.

Then, the context creates the beans. With every definition in place, the context works through them and creates each bean. For a @Service that means calling its constructor; for our configuration it means invoking myService(). As each object is created, other post-processors get a chance to work on it: injecting @Autowired dependencies, running @PostConstruct methods, or wrapping the object in a proxy.

This split is why Boot's auto-configuration can check whether you've already defined a bean before adding its own.

Why we need Spring Boot 4 and a little history.

Every major version of Spring has been an answer to whatever hurt the most at the time. Knowing what each generation was reacting to explains Boot 4 far better than a list of new features.

2002–2009: Spring as the way out of J2EE.

Spring started as a reaction. In October 2002, Rod Johnson published Expert One-on-One J2EE Design and Development, and the framework he released alongside the book reached version 1.0 in March 2004. The idea was that plain Java objects wired together by a container were enough – you didn't need the heavyweight component model J2EE prescribed. That same year, Martin Fowler gave the wiring pattern its name: dependency injection.

The wiring lived in XML, and the application was packaged as a WAR and deployed into an application server that somebody else ran. The server owned the application. In December 2009, Spring 3.0 introduced @Configuration, and the wiring started moving from XML into Java.

2012–2014: Boot flips who owns whom.

In October 2012, Mike Youngstrom opened SPR-9888, "Improved support for 'containerless' web application architectures". His argument was that developers who understood servlet containers were becoming rare, and that Spring's dependence on them made Spring harder to learn. He wanted a Spring application that started from a plain main() method. He wasn't alone – a couple of months earlier, James Ward had written about running Spring MVC without a container.

The first milestone followed in August 2013, with the stated goal of providing "a radically faster and widely accessible 'getting started' experience for all Spring development". Eighteen months after the issue was opened, on 1 April 2014, that request shipped as Spring Boot 1.0. It flipped the relationship: the application now owns the server – you saw the context start Tomcat earlier in this article. It also introduced the conceptual model Boot has been known for ever since: the classpath is the configuration. Put a library on the classpath, and Boot configures it for you.

2018: a second way to serve a request.

Spring Boot 2.0 arrived in March 2018 on Spring Framework 5, and mostly extended the model rather than changing it – a Java 8 baseline, Micrometer metrics, Kotlin support, a rebuilt actuator. And then, WebFlux, a second, non-blocking web stack sitting alongside Spring MVC.

Where Spring MVC needs a servlet container, WebFlux defaults to Netty, which isn't a servlet container. It's "an asynchronous event-driven network application framework" – it speaks HTTP over sockets itself, handling many connections on a small pool of event-loop threads instead of dedicating a thread to each request. It implements no Jakarta specification, so there's no jakarta.servlet.Servlet and no DispatcherServlet anywhere in a WebFlux application. Spring provides its own equivalents instead: DispatcherHandler in place of DispatcherServlet, WebFilter in place of a servlet Filter, and ServerHttpRequest in place of HttpServletRequest.

You can see it in the dependency tree. A WebFlux application pulls in Netty and Reactor, and contains no servlet API and no Tomcat at all. Boot notices that the servlet classes are missing and builds a reactive context that starts Netty rather than a servlet container. That's the split you still choose between when you pick spring-boot-starter-webmvc over spring-boot-starter-webflux, and it's the one part of Spring's web story that doesn't rest on a Jakarta specification – the reactive stack is built on Reactive Streams, which isn't a Jakarta EE standard.

2022: Boot 3 breaks with the past.

Spring Boot 3.0, released in November 2022, was the first major version since 2.0 four and a half years earlier, and it was deliberately disruptive. It moved the baseline to Java 17, and it moved from javax to jakarta: when Java EE was handed over to the Eclipse Foundation, the javax namespace stayed behind, so every Jakarta EE package was renamed and virtually every application had to change its imports.

Boot 3 also introduced GraalVM native images, which required a new way of thinking. A native image is built ahead of time under a closed-world assumption, so Spring had to work out at build time what an application contains, rather than discovering everything at startup.

2025: Boot 4 makes the boundaries explicit.

Spring Boot 4.0 arrived on 20 November 2025, described by the team as "the beginning of a new Spring Boot generation providing solid foundations for coming years." Where Boot 3 was a break with the past, Boot 4 is a correction of Boot's own model.

"The classpath is the configuration" had a cost. Up to Boot 3, the auto-configuration for nearly every supported technology lived in a single spring-boot-autoconfigure jar, which grew from 182 KiB in Boot 1.0 to 2 MiB by Boot 3.5. Every application carried configuration code for technologies it never used, and whether a technology got configured depended on what happened to be on the classpath.

Boot 4 splits that jar into a module per technology. You now declare what you use – Flyway, for instance, needs spring-boot-starter-flyway, where the Flyway jar alone used to be enough – and each starter has a matching test starter. Configuration for technologies you don't use isn't on your classpath at all. This is also what makes the layers from the start of this article visible: spring-boot-webmvc and spring-webmvc are now separate jars with separate jobs.

The same idea runs through the rest of the release. Null safety through JSpecify makes nullness part of the API contract across the Spring portfolio, rather than something you find out at runtime. The last of the old javax.annotation and javax.inject support is gone, Jackson moves to version 3, and the baseline is Jakarta EE 11, with first-class support for Java 25 while keeping Java 17 compatibility.

The explicitness has a price, and Boot acknowledges it: spring-boot-starter-classic exists to restore the old everything-on-the-classpath behaviour while you migrate.

So, why Boot 4?

If Boot 1 said stop configuring, start running, and Boot 3 said leave the past behind, Boot 4 says make the opinion visible. For someone who wants to understand Spring rather than just use it, that makes Boot 4 the best version yet to learn on – the boundaries between Framework, projects, Boot, and starters that this article describes are now boundaries you can see in your own dependency tree.

Let's Stay in touch

You're always welcome to reach out at nicklas@mjukvare.com!

Mjukvare

© 2026