Spring Boot 3 Complete Guide: New Features and Migration (2026)

Spring Boot 3 is the biggest major release since Boot 2.0. It sets Java 17 as the minimum baseline, migrates to Jakarta EE 10, ships first-class GraalVM native image support, overhauls observability, and introduces declarative HTTP clients. This guide covers every major change and how to migrate safely from Boot 2.x.

Java 17 Baseline and What It Means

Spring Boot 3.x requires Java 17 as the minimum runtime. This is not just a version bump — it unlocks record classes, sealed classes, pattern matching for instanceof, text blocks, and switch expressions across the entire Spring ecosystem.

// Spring Boot 3 leverages Java 17 features throughout
// Records as DTOs — no Lombok needed
public record UserRequest(String username, String email, String role) {}

// Pattern matching in controller logic
public ResponseEntity<?> handleEvent(Object event) {
    return switch (event) {
        case OrderPlaced o  -> ResponseEntity.ok(processOrder(o));
        case PaymentFailed p -> ResponseEntity.status(402).body(p.reason());
        case UserRegistered u -> ResponseEntity.status(201).body(u.userId());
        default -> ResponseEntity.badRequest().build();
    };
}

Jakarta EE 10 Migration

This is the most disruptive change for existing codebases. Every javax.* package moved to jakarta.*. This affects annotations you use daily:

Old (javax.*)New (jakarta.*)
javax.persistence.*jakarta.persistence.*
javax.validation.*jakarta.validation.*
javax.servlet.*jakarta.servlet.*
javax.annotation.*jakarta.annotation.*
javax.transaction.*jakarta.transaction.*

Run the OpenRewrite migration recipe to automate the bulk of the rename:

# Add OpenRewrite to pom.xml plugin section, then run:
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:LATEST \
  -Drewrite.activeRecipes=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0
Tip: After running OpenRewrite, do a project-wide search for remaining javax. imports — the recipe handles most cases but custom code or third-party library re-exports can still sneak through.

GraalVM Native Image Support

Spring Boot 3 ships with deep AOT (ahead-of-time) processing support. At build time, Spring analyses your bean definitions and generates initialization code and reflection metadata — allowing GraalVM to compile the application to a native binary that starts in under 100ms and uses far less memory.

<!-- pom.xml: add the native profile -->
<profiles>
  <profile>
    <id>native</id>
    <build>
      <plugins>
        <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <configuration>
            <image>
              <builder>paketobuildpacks/builder-jammy-tiny</builder>
            </image>
          </configuration>
          <executions>
            <execution>
              <id>process-aot</id>
              <goals><goal>process-aot</goal></goals>
            </execution>
          </executions>
        </plugin>
        <plugin>
          <groupId>org.graalvm.buildtools</groupId>
          <artifactId>native-maven-plugin</artifactId>
          <executions>
            <execution>
              <id>add-reachability-metadata</id>
              <goals><goal>add-reachability-metadata</goal></goals>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </build>
  </profile>
</profiles>
# Build native executable
mvn -Pnative native:compile

# Or build native container image (no GraalVM local install needed)
mvn -Pnative spring-boot:build-image

Micrometer Observability

Spring Boot 3 replaces the fragmented metrics/tracing/logging setup with a unified observability layer built on Micrometer. The @Observed annotation and ObservationRegistry API produce metrics, traces, and log correlation automatically.

// application.yml
management:
  tracing:
    sampling:
      probability: 1.0  # 100% sampling in dev; reduce in prod
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans

// Service annotated with @Observed — generates metrics + trace spans automatically
@Service
@Observed(name = "order.service")
public class OrderService {

    public Order createOrder(CreateOrderRequest req) {
        // A span named "order.service.createOrder" is created automatically
        // Metrics: http.server.requests timer is also updated
        return orderRepository.save(new Order(req));
    }
}

// Manual observation for fine-grained control
@Service
public class InventoryService {
    private final ObservationRegistry registry;

    public InventoryService(ObservationRegistry registry) {
        this.registry = registry;
    }

    public int checkStock(String sku) {
        return Observation.createNotStarted("inventory.check", registry)
            .lowCardinalityKeyValue("sku.category", extractCategory(sku))
            .observe(() -> inventoryRepo.getStock(sku));
    }
}

Problem Details for HTTP APIs (RFC 9457)

Spring Boot 3 implements RFC 9457 (formerly RFC 7807) Problem Details out of the box. Instead of returning raw error strings, your API returns a structured JSON problem description.

// Enable in application.yml
spring:
  mvc:
    problemdetails:
      enabled: true

// Spring Boot now returns this automatically for unhandled exceptions:
// {
//   "type": "about:blank",
//   "title": "Not Found",
//   "status": 404,
//   "detail": "No order found with id 42",
//   "instance": "/api/orders/42"
// }

// Custom ProblemDetail in your controller
@RestController
public class OrderController {

    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable long id) {
        return orderService.findById(id).orElseThrow(() -> {
            ProblemDetail pd = ProblemDetail.forStatusAndDetail(
                HttpStatus.NOT_FOUND,
                "No order found with id " + id
            );
            pd.setTitle("Order Not Found");
            pd.setProperty("orderId", id);
            pd.setProperty("timestamp", Instant.now());
            return new ResponseStatusException(HttpStatus.NOT_FOUND, null, null) {
                @Override public ProblemDetail getBody() { return pd; }
            };
        });
    }
}

HTTP Interface Clients

Spring Boot 3 ships declarative HTTP clients — similar to Feign but built into Spring, requiring no extra dependency. Annotate an interface with @HttpExchange and Spring generates the implementation.

// Define the client interface
@HttpExchange("https://api.github.com")
public interface GitHubClient {

    @GetExchange("/users/{username}")
    GitHubUser getUser(@PathVariable String username);

    @GetExchange("/repos/{owner}/{repo}/issues")
    List<Issue> getIssues(@PathVariable String owner,
                           @PathVariable String repo,
                           @RequestParam int perPage);

    @PostExchange("/repos/{owner}/{repo}/issues")
    Issue createIssue(@PathVariable String owner,
                      @PathVariable String repo,
                      @RequestBody CreateIssueRequest body);
}

// Register it as a bean
@Configuration
public class HttpClientConfig {

    @Bean
    public GitHubClient gitHubClient() {
        WebClient webClient = WebClient.builder()
            .baseUrl("https://api.github.com")
            .defaultHeader("Authorization", "Bearer " + token)
            .build();
        HttpServiceProxyFactory factory = HttpServiceProxyFactory
            .builderFor(WebClientAdapter.create(webClient))
            .build();
        return factory.createClient(GitHubClient.class);
    }
}

// Inject and use like any other Spring bean
@Service
public class RepoService {
    private final GitHubClient github;
    public RepoService(GitHubClient github) { this.github = github; }

    public GitHubUser fetchUser(String username) {
        return github.getUser(username);
    }
}

Spring Security 6

Spring Security 6 removed the deprecated WebSecurityConfigurerAdapter (you must migrate to SecurityFilterChain beans) and changed several defaults.

// Spring Boot 2.x — DEPRECATED, removed in Security 6
@Configuration
public class OldSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN");
    }
}

// Spring Boot 3 / Security 6 — correct approach
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/login?logout")
            )
            .csrf(csrf -> csrf
                .ignoringRequestMatchers("/api/**")  // disable for REST endpoints
            );
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Virtual Threads in Boot 3.2+

Boot 3.2 added a single property to enable virtual threads across the entire application stack — Tomcat request handling, @Async tasks, and Spring MVC blocking operations all use virtual threads automatically:

spring:
  threads:
    virtual:
      enabled: true

With this enabled, HikariCP (the default connection pool) should be tuned down — virtual threads wait efficiently, so you do not need a large pool. Recommended starting point: spring.datasource.hikari.maximum-pool-size=10.

Migration Pitfalls Table (Boot 2.x → Boot 3.x)

IssueBoot 2.xBoot 3.x Fix
javax.* imports won't compilejavax.persistence, javax.validation, etc.Replace all with jakarta.* — use OpenRewrite
WebSecurityConfigurerAdapter missingExtend WebSecurityConfigurerAdapterDeclare SecurityFilterChain @Bean
antMatchers removedhttp.authorizeRequests().antMatchers()http.authorizeHttpRequests().requestMatchers()
Spring Data repository changesCrudRepository.findById returns OptionalSame — no change needed
Actuator endpoint paths/actuator/health worksSame, but some endpoint IDs renamed
Logging pattern changeCustom log patternsMicrometer trace IDs now auto-injected in MDC
Spring Batch 5JobBuilderFactory, StepBuilderFactoryDeprecated; use JobBuilder(name, repo) directly

FAQ

Can I run Spring Boot 3 on Java 11?
No. Java 17 is the hard minimum. If you cannot upgrade your JDK, stay on Spring Boot 2.7 (supported with security patches until 2025) and plan the JDK upgrade first.
Do I need GraalVM installed to use Spring Boot 3?
No. GraalVM native image is an opt-in feature. Regular Spring Boot 3 apps compile and run on standard JDK 17+ with no changes. You only need GraalVM if you want to produce a native binary.
Is Feign still supported in Spring Boot 3?
Spring Cloud OpenFeign is still available, but the new built-in HTTP Interface clients are the recommended approach for new projects. They require no extra dependency and integrate directly with Spring's WebClient and RestClient.
What happened to RestTemplate in Boot 3?
RestTemplate is not removed but is in maintenance mode. The new RestClient (introduced in Boot 3.2) is a synchronous, fluent HTTP client that replaces RestTemplate. Migrate when convenient — Boot 3.2's RestClient API is much cleaner.
How do I verify my app is correctly instrumented with Micrometer?
Hit /actuator/metrics to see available metrics and /actuator/health to see component health. For traces, configure a Zipkin or Jaeger exporter and verify spans appear. The /actuator/loggers endpoint shows MDC trace ID injection is working when log lines contain traceId and spanId fields.