Skip to main content
Back to Blog

Spring Boot 4 and Java 21: The Enterprise Upgrade Playbook for 2026

Spring Boot 4 migration, Java 21 virtual threads, observability with OpenTelemetry, and GraalVM native image tradeoffs for enterprise teams still on Boot 3.x.

·spring-boot, java, backend, enterprise

Version Landscape

VersionOSS support endsAction
4.0.xDec 31, 2026Target for new services
3.5.xJun 30, 2026Patch now, plan migration
3.4.xDec 31, 2025Already EOL

Boot 4 ships Spring 7, Jakarta EE 11, Hibernate 7, Jackson 3, and JUnit 6. Patch to latest 3.5.x, clear deprecation warnings, then migrate. A phased rollout on Kubernetes is normal. You do not need virtual threads or native image on day one.

Boot 4 Upgrade Essentials

Fix these before bumping the Boot version. They are what actually stall migrations.

Web and REST client starters

XML

spring-boot-starter-web pulled in REST clients, Flyway, and WebClient transitively.

Boot 4 splits autoconfiguration into focused modules. Declare each capability explicitly.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-restclient</artifactId>
</dependency>

Stateless API security

Java

authorizeRequests() with antMatchers() and session-based auth were common even on REST APIs.

Spring Security 7 removes authorizeRequests(). Stateless JWT resource servers should not create HttpSessions.

@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
  http.authorizeHttpRequests(auth -> auth
      .requestMatchers("/actuator/health").permitAll()
      .requestMatchers("/api/**").authenticated()
      .anyRequest().denyAll())
    .sessionManagement(s -> s
      .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    .oauth2ResourceServer(oauth2 -> oauth2.jwt());
  return http.build();
}

Jackson 3 imports

Java

Jackson 2 used the com.fasterxml.jackson package namespace.

Jackson 3 relocates to tools.jackson. Search-replace imports and retest custom serializers.

import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.annotation.JsonProperty;

Virtual Threads in Production

Virtual threads (JEP 444, Java 21) let you keep blocking imperative code while scaling concurrent I/O. Oracle's docs are explicit: they improve throughput, not single-request latency.

Client
Ingress
Spring MVC
Virtual Thread
JDBC
Database
Blocking request path with virtual threadsbottleneck shifts to pools

Enable virtual threads

YAML

Teams tuned Tomcat max threads and min-spare to handle concurrent blocking requests.

One property routes servlet handling through virtual threads. Recalibrate HikariCP because the database pool becomes the new ceiling under load.

spring:
  threads:
    virtual:
      enabled: true
  main:
    keep-alive: true
  datasource:
    hikari:
      maximum-pool-size: 50
      connection-timeout: 20000

When NOT to use virtual threads

This is the discussion senior teams are having in 2026. Virtual threads are not a universal upgrade.

Good fitPoor fit
HTTP calls to downstream servicesCPU-heavy computation (parsing, encryption, ML inference)
JDBC and JPA queriesLarge synchronized blocks around I/O
File I/O and blocking messagingLibraries that pin carrier threads
Legacy blocking code you want to keep readableHeavy lock contention across threads

Thread pinning is the main production risk. When a virtual thread enters a synchronized block or a native JNI call during blocking I/O, the JVM may pin it to a carrier OS thread, negating the scalability benefit. Audit with JFR (jdk.VirtualThreadPinned) before celebrating throughput gains. On Java 24+, JEP 491 reduces pinning in synchronized, but third-party libraries can still pin.

Spring MVC vs WebFlux

ScenarioMVC + virtual threadsWebFlux
CRUD APIs with JDBC/JPAYes
Streaming or SSE endpointsYes
End-to-end non-blocking with R2DBCYes
Team readability and debuggabilityYes
Explicit backpressure as a product featureYes

For newly developed blocking I/O services, Spring MVC + virtual threads is becoming the preferred baseline architecture, replacing much of the complexity previously justified by reactive stacks. WebFlux remains the right tool when the stack is non-blocking end to end.

Observability, AOT, and Native Image

Modern production observability is four signals, not one endpoint.

Metrics
Traces
Logs
Profiles
Production observability stackunified via OpenTelemetry

Micrometer Observation is Spring's single instrumentation surface for metrics and traces. OpenTelemetry is the export standard. A typical Grafana stack in 2026: Prometheus for metrics (with exemplars linking to traces), Tempo for distributed traces, Loki for logs, and Pyroscope or eBPF-based agents for continuous profiling.

Observability baseline

YAML

Many teams exposed only /actuator/health and wired monitoring ad hoc per service.

Expose Prometheus metrics, sample traces, and propagate context across @Async boundaries. Exemplars let you jump from a latency spike in Grafana to the exact trace in Tempo.

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  tracing:
    sampling:
      probability: 0.1
  otlp:
    tracing:
      endpoint: http://otel-collector:4318/v1/traces

spring:
  task:
    execution:
      propagate-context: true

AOT processing

Boot 4 doubles down on ahead-of-time preparation. Run at build time to reduce reflection metadata and warm the bean graph before deployment:

./mvnw spring-boot:process-aot

AOT reduces startup reflection, prepares the application context for native image compilation, and catches closed-world constraint violations early.

GraalVM native image: worth it?

JVM deploymentNative image
StartupSecondsMilliseconds
Memory footprintHigher baselineLower, denser packing
Peak throughputOften higherSometimes lower
Reflection-heavy libsFineRequires hints or tracing agent
Dynamic config refreshSupportedNot supported (Spring Cloud docs)

Use native image when: serverless or scale-to-zero, cold starts matter, memory-constrained Kubernetes nodes, or CLI/batch tools.

Avoid native image when: reflection-heavy dependency graphs, runtime classpath variation, Spring Cloud context refresh, or peak throughput matters more than startup time.

Native image is an operations decision, not a purity contest. Profile both paths on your actual dependency graph before committing.

Migration

Patch 3.5
Fix Deprecations
Swap Container
Boot 4
Pilot VT
One stage per sprint
  1. Patch 3.5 to latest. Zero deprecation warnings in CI.
  2. Fix deprecations via the Boot 4.0 migration guide.
  3. Swap Undertow to Tomcat or Jetty while still on 3.5.
  4. Upgrade to Boot 4 and test Jackson 3 + Security 7.
  5. Pilot virtual threads on one service with JFR pinning audit and realistic load tests.
The rule

Get on a supported version first. For newly developed blocking I/O services, Spring MVC + virtual threads is becoming the preferred baseline architecture, replacing much of the complexity previously justified by reactive stacks.