[MM’s] Boot Notes — Spring Boot 4
The Next Generation of Modern Java Services

Every few years, Spring delivers a release that quietly resets the foundation of enterprise Java development. Spring Boot 4.0 — built on top of Spring Framework 7 — is exactly that kind of release. It’s not just an upgrade; it’s a modernization of the entire development model: modularized starters, native API versioning, declarative HTTP clients, built-in resilience, JSpecify null-safety and first-class support for Java 25.
If you’ve been using Spring Boot 3.x, you’ll feel at home. But look closer and you’ll notice the framework has become leaner, more explicit, and better aligned with how teams build microservices today.
⚡ TL;DR (Quick Recap)
- Modularized Boot → Precise, smaller modules replace monolithic starters.
- API Versioning → Native, annotation-based version routing without custom filters.
- HTTP Service Clients → Declarative, Feign-like interfaces powered by Spring’s @HttpExchange.
- First-party Resilience → @Retryable, @ConcurrencyLimit and retry templates built directly into Spring Framework.
- JSpecify Null Safety → Portfolio-wide null-safety with modern, tool-friendly annotations.
- Java 25 & Jackson 3 → Full support for the newest JVM and JSON stack while keeping Java 17 compatibility.
Why Spring Boot 4 Matters Right Now
Spring Boot 3.x brought major changes — Jakarta EE 10, virtual threads, observability — but remained constrained by older assumptions: large starters, custom API versioning patterns and resilience handled by external libraries.
Spring Boot 4 tackles these at the architectural level:
- Clean module boundaries
- Explicit null-safety
- Native versioning across MVC, WebFlux, clients and tests
- Modern client APIs for HTTP, JDBC, JMS
- Consistent infrastructure for large, service-heavy systems
The release aligns Spring Boot with modern Java expectations: composable, type-safe, tuned for both JVM and native image deployments.
1. Modularization: Boot Becomes Lean and Precise
Classic starters often pulled in more than teams needed. “Starter bloat” wasn’t dramatic, but it made container images heavier and dependency trees noisy.
The Spring Boot 4 approach
Spring Boot 4 introduces complete modularization across 70+ modules. Every technology now has:
- a main module
- a dedicated starter
- a dedicated test starter
<!-- Old -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- New -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jackson</artifactId>
</dependency>
Why it matters
- Faster container builds
- Smaller runtime footprint
- Clearer dependency graphs
- Better test isolation
- More predictable auto-configuration
For teams upgrading gradually, Spring Boot provides classic starters (spring-boot-starter-classic and spring-boot-starter-test-classic) to preserve older behavior while you transition.
2. Native API Versioning: Declarative, Clear, Zero Boilerplate
API versioning has historically been one of the most DIY aspects of Spring development. Spring Framework 7 introduces native, annotation-based versioning:
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping(path = "/{id}", version = "1.0")
ProductV1 getV1(@PathVariable String id) { ... }
@GetMapping(path = "/{id}", version = "2.0")
ProductV2 getV2(@PathVariable String id) { ... }
}
Select your strategy via properties
# Option A: Header-based
spring.mvc.apiversion.use.header=API-Version
# Option B: Query parameter
spring.mvc.apiversion.use.query-param=version
# Option C: Path segment
spring.mvc.apiversion.use.path-segment=true
Baseline versions
A powerful addition:
@GetMapping(version = "1.1+")
…matches versions ≥ 1.1 unless a more specific mapping exists.
End-to-end support
Versioning is also integrated into:
- RestClient
- WebClient
- HTTP interface clients
- MockMvc & WebTestClient
var client = RestClient.builder()
.apiVersionInserter(ApiVersionInserter.useHeader("API-Version"))
.build();
client.get()
.uri("/api/products/123")
.apiVersion("2.0")
.retrieve()
.body(ProductV2.class);
This is one of the most impactful quality-of-life improvements in Spring in years.
3. HTTP Service Clients: Declarative HTTP Without Feign
RestTemplate is deprecated (officially in 7.1), WebClient is powerful, but verbose and many teams brought Feign into the stack for simplicity.
Spring Framework introduces HTTP Service Client Enhancements — a declarative interface-based API:
@HttpExchange(url = "https://api.example.com")
public interface ProductService {
@GetExchange("/products")
List<Product> getAllProducts();
@GetExchange("/products/{id}")
Product get(@PathVariable String id);
@PostExchange("/products")
Product create(@RequestBody Product product);
}
Register multiple interfaces at once
@Configuration
@ImportHttpServices(
group = "external",
types = {ProductService.class, OrderService.class}
)
class HttpServicesConfig extends AbstractHttpServiceRegistrar {
@Bean
RestClientHttpServiceGroupConfigurer groupConfigurer() {
return groups -> groups.filterByName("external")
.configureClient((group, builder) ->
builder.defaultHeader("User-Agent", "My-App"));
}
}
Why this matters
- Zero boilerplate client creation
- Native Spring integration
- Group-level configuration
- Works with RestClient & WebClient
- No external Feign dependency
4. Built-In Resilience: Retries and Concurrency Limits Without Extra Libraries
For years, most Spring users relied on Spring Retry or Resilience4j for retry logic.
In Spring Framework 7, resilience is now first-party.
Annotated methods
@Service
public class ExternalApiService {
@Retryable(
includes = GatewayTimeoutException.class,
maxRetries = 4,
multiplier = 2.0
)
@ConcurrencyLimit(15)
public ApiResponse callExternal(String key) {
return client.get(key);
}
}
Programmatic alternative
var policy = RetryPolicy.builder()
.includes(GatewayTimeoutException.class)
.maxRetries(4)
.delay(Duration.ofMillis(200))
.build();
var template = new RetryTemplate(policy);
return template.execute(() -> client.get(key));
Reactive support
If the return type is reactive, the retry logic automatically decorates the Reactor pipeline.
When to use what:
- Built-in Spring resilience → Most apps (simple retries, limits)
- Resilience4j → Circuit breakers, rate limiting, bulkheads
5. JSpecify Null Safety: A Modern, Tool-Friendly Type System
Spring has fully migrated from JSR-305 to JSpecify annotations.
This brings:
- Better Kotlin alignment
- Full generic/vararg/array support
- Precise nullability contracts
- IDE + static analysis improvements
@NonNull
List<@Nullable String> items;
Kotlin users especially benefit: API nullability is now accurately inferred. This change appears small, but it dramatically reduces subtle bugs across large codebases.
6. JmsClient: A Modern Client API for Messaging
After RestClient and JdbcClient, messaging now gets the same treatment.
@Service
public class OrderMessaging {
private final JmsClient jms;
public void sendOrder(Order order) {
jms.send("orders.queue")
.withPayload(order)
.execute();
}
public Order receiveOrder() {
return jms.receive("orders.queue")
.asType(Order.class);
}
}
It’s simpler and more discoverable than JmsTemplate and aligns with Spring’s modern fluent client APIs.
Other Noteworthy Changes in Spring Boot 4 and Spring Framework 7
Major dependency upgrades
- Jackson 3 (with Jackson 2 compatibility mode available)
- Hibernate ORM 7.1/7.2 with JPA 3.2
- Servlet 6.1 baseline → Tomcat 11, Jetty 12.1
- Kotlin 2.2
- GraalVM 25 (new exact reachability metadata format)
Removals
- Undertow support
- All javax.* annotations
- Spock integration (due to Groovy 5)
- Executable Uber-JAR launch scripts
- Several deprecated Boot 3.x APIs
Testing improvements
- RestTestClient (non-reactive equivalent of WebTestClient)
- Better @Nested test class injection
- Dedicated test starters for every technology
Configuration changes
- Many MongoDB properties renamed
- HttpMessageConverters is deprecated in favor of Spring Framework 7’s improved support
- Tracing properties updated
Migration Guide: A Practical Path Forward
Upgrading to Boot 4 sounds big — but the migration path is smooth:
1. Upgrade to Spring Boot 3.5.x first
This ensures you’re on the correct baseline.
2. Remove deprecated APIs
Anything deprecated in 3.x is removed in 4.0.
3. Switch to classic starters
Temporarily use:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-classic</artifactId>
</dependency>
Then gradually replace with the new modular starters.
4. Migrate javax.* → jakarta.*
If you haven’t already.
5. Adopt JSpecify
Update nullability imports if needed.
6. Move to HTTP Service Clients & native API versioning
Highly recommended for new services.
Final Takeaways
Spring Boot 4 marks the beginning of a new generation for the Spring ecosystem. It’s lighter, more explicit, more modular and better aligned with the practices modern teams rely on.
If you’re starting a new service:
→ Start with Boot 4. You’ll get versioning, declarative clients, modern resilience and type safety out of the box.
If you’re maintaining existing systems:
→ Upgrade gradually. Use classic starters, follow the migration guide and adopt new modules at your own pace.
If you’re working across multiple microservices:
→ Group your HTTP clients, version your APIs natively and reduce the amount of custom wiring you maintain.
Spring Boot 4 is not a radical rewrite — it’s the framework evolving gracefully without breaking the familiar Spring development experience.
You can find an example of how to use Spring Boot 4 on GitHub.
Originally posted on marconak-matej.medium.com.