If you're a Java developer like me, you've probably noticed how Spring Boot has become the go-to framework for building modern Java applications. With Spring Boot 4.0 and Spring Framework 7 which arrived in November 2025, we're standing at another major milestone in this journey.
Spring Boot 4 and Spring Framework 7 aren't just regular updates. They're transformative releases that will reshape how we build, deploy, and maintain Java applications. In this article, we are trying to understand what's changing, why it matters, and how you can prepare for this transition.
What is Changing at the Platform Level
Java Version: Time to Move Forward
Let's start with the elephant in the room—Java versions.
- Minimum requirement: Java 17 (still holding strong)
- Recommended version: Java 25 (the latest LTS release coming in September 2025)
What does this mean? If you're still running Java 11 or 8, it's time to seriously plan your upgrade. Java 17 brings features like records, sealed classes, and strong pattern matching that can significantly improve your code quality. Java 25 takes this further with advanced performance optimizations and new APIs that Spring 7 is designed to take advantage of.
Simple tip: Start testing your applications with Java 21 LTS right now. It's a safe middle ground and will prepare you for Java 25 migration.
Jakarta EE 11: The "Jakarta is Here" Moment
Remember when Spring Boot 3 migrated from javax.* to jakarta.* packages? This time, Spring Framework 7 fully embraces Jakarta EE 11 with deeper integration:
- Servlet 6.1 with modern web APIs
- JPA 3.2 and Hibernate ORM 7.0 for better database operations
- Bean Validation 3.1 with improved support for Kotlin records
If you haven't migrated from javax to jakarta yet, Spring Boot 4 won't work for you. This is a non-negotiable requirement.
Spring Framework 7 - The Developer Experience Revolution
Feature 1: Built-in Resilience Without Extra Dependencies
The Problem: Previously, you needed the spring-retry external project to add retry capabilities to your application.
The Solution: Spring Framework 7 brings resilience features directly into the core framework.
Working with @Retryable
@Service
public class PaymentService {
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
public TransactionResult processPayment(Payment payment) {
// Call external payment gateway
return externalGateway.charge(payment);
}
@Recover
public TransactionResult recover(RuntimeException ex, Payment payment) {
logger.error("Payment failed after retries for: " + payment.getId());
return new TransactionResult(false, "Payment failed");
}
}
Notice how you don't need any external project for this? Just use the annotation, configure your retry policy, and Spring handles the rest. The @Recover method runs when all retries are exhausted.
Controlling Concurrency with @ConcurrencyLimit
@Service
public class ReportGenerationService {
@ConcurrencyLimit(value = 5)
public Report generateDetailedReport(ReportQuery query) {
// This method will never run more than 5 times concurrently
// Other requests will queue up
return complexReportCalculation(query);
}
}
Imagine you have a report generation service that's very resource-intensive. Without @ConcurrencyLimit, 100 simultaneous requests would crash your server. With this annotation, only 5 can run at the same time, and the rest wait gracefully.
How to enable these features:
@Configuration
@EnableResilientMethods
public class ResilienceConfig {
// Your other beans here
}
Feature 2: API Versioning Without Hacks
The Old Way: You had to create separate endpoints for each API version or use complex URL patterns.
The New Way: Spring Framework 7 lets you define API versions declaratively.
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
@RequestMapping(produces = "application/vnd.api+json;version=1")
public List<UserV1> getUsersV1() {
// Version 1 returns limited fields
return userService.getUsersV1();
}
@GetMapping
@RequestMapping(produces = "application/vnd.api+json;version=2")
public List<UserV2> getUsersV2() {
// Version 2 returns enhanced data
return userService.getUsersV2();
}
}
The framework automatically routes requests to the correct version based on media type. This makes versioning explicit and maintainable.
Feature 3: Programmatic Bean Registration for Dynamic Applications
Use Case: Your application needs to register beans based on configuration files or external systems at runtime.
@Configuration
public class DynamicBeanConfiguration implements BeanRegistrar {
@Override
public void registerBeans(BeanRegistrationContext context) {
ConfigurationService config = context.getBeanFactory()
.getBean(ConfigurationService.class);
config.getDataSources().forEach(datasource -> {
context.registerBean(
"datasource_" + datasource.getName(),
DataSource.class,
() -> createDataSource(datasource)
);
});
}
private DataSource createDataSource(DatasourceConfig config) {
// Create datasource based on configuration
return new DynamicDataSource(config);
}
}
Instead of defining all beans statically in XML or annotations, you can now create them dynamically. This is incredibly useful for multi-tenant applications or plugin-based architectures.
Feature 4: Better Null Safety with JSpecify
Spring Framework 7 now uses JSpecify annotations for improved null safety across the framework. This means:
@Service
public class CustomerService {
public @Nullable Customer findCustomer(@NonNull String customerId) {
// This method promises:
// - It will never accept a null customerId
// - It might return null (hence @Nullable)
return customerRepository.findById(customerId).orElse(null);
}
}
IDEs and static analysis tools can now detect potential null pointer exceptions before runtime. It's like having an extra pair of eyes watching your code.
Feature 5: Optional Support in Spring Expression Language (SpEL)
Before: Handling Optional values in SpEL was awkward.
After: Spring Framework 7 treats Optional naturally:
@Service
public class NotificationService {
@Value("#{userService.findUserById(#userId)?.name ?: 'Guest User'}")
private String userName;
public void sendNotification(String userId, String message) {
// The Elvis operator (?:) provides a default
// The safe navigation (?.) prevents null exceptions
notificationCenter.send(userName, message);
}
}
If the Optional is empty, it gracefully falls back to "Guest User" without throwing exceptions.
Feature 6: Enhanced HTTP Clients with @ImportHttpServices
The Scenario: Your microservice talks to 5 different external APIs. Managing all these clients is messy.
@Configuration
@ImportHttpServices(
group = "external-services",
types = {WeatherApiClient.class, EmailServiceClient.class, PaymentApiClient.class}
)
public class HttpClientConfiguration {
// Spring automatically creates proxy beans for these interfaces
}
// Define your HTTP clients as interfaces
@HttpService(name = "weather-api", url = "https://api.weather.service")
public interface WeatherApiClient {
@GetExchange("/forecast/{city}")
WeatherForecast getForecast(@PathVariable String city);
}
// Use them like regular Spring beans
@Service
public class WeatherAnalysisService {
private final WeatherApiClient weatherClient;
public WeatherAnalysisService(WeatherApiClient weatherClient) {
this.weatherClient = weatherClient;
}
public void analyzeTrends(String city) {
WeatherForecast forecast = weatherClient.getForecast(city);
// Process forecast
}
}
Spring automatically converts these interfaces into fully-functional HTTP clients. No more boilerplate RestTemplate or WebClient setup code!
Feature 7: Stream Processing in HTTP Clients
Working with large files or streaming data is now easier:
@HttpService(url = "https://data-service.com")
public interface DataStreamClient {
@GetExchange(value = "/download/large-dataset", produces = "application/octet-stream")
InputStream downloadLargeFile();
@PostExchange(value = "/upload/stream", consumes = "application/octet-stream")
void uploadLargeFile(OutputStream outputStream);
}
Perfect for handling large file uploads and downloads without loading everything into memory.
Feature 8: JdbcClient and JmsClient Enhancements
Spring Framework 7 introduces fluent APIs for database and messaging operations:
@Service
public class OrderRepository {
private final JdbcClient jdbcClient;
public List<Order> findOrdersByCustomer(String customerId) {
return jdbcClient.sql(
"SELECT * FROM orders WHERE customer_id = ? ORDER BY created_at DESC"
)
.param(1, customerId)
.map((rs, rowNum) -> new Order(
rs.getString("id"),
rs.getString("status"),
rs.getTimestamp("created_at").toLocalDateTime()
))
.list();
}
}
@Service
public class NotificationPublisher {
private final JmsClient jmsClient;
public void publishOrderNotification(Order order) {
jmsClient.send("notification-queue")
.priority(9)
.correlationId(order.getId())
.body(new OrderNotificationMessage(order))
.send();
}
}
No more verbose JdbcTemplate or MessagingTemplate code. Everything is fluent and readable.
Feature 9: Centralized HTTP Message Converter Configuration
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(
HttpMessageConverters.ServerBuilder builder) {
builder.jsonMessageConverter(
new JacksonJsonHttpMessageConverter(
JsonMapper.builder()
.featuresToEnable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build()
)
);
}
}
Manage all serialization and deserialization logic in one place. This ensures consistency across your entire application.
Feature 10: RestTestClient for Testing REST APIs
Testing REST endpoints is now cleaner:
@WebMvcTest(OrderController.class)
public class OrderControllerTest {
@Autowired
private RestTestClient restTestClient;
@Test
public void testGetOrderDetails() {
restTestClient.get()
.uri("/api/orders/{id}", "ORDER-123")
.exchange()
.expectStatus().isOk()
.expectBody(Order.class)
.isEqualTo(expectedOrder);
}
}
The API is fluent, type-safe, and mirrors the RestTemplate API that developers already know.
Feature 11: Improved Path Matching Patterns
@RestController
public class DocumentController {
// Matches: /files/documents/guide.pdf or /files/docs/readme.txt
@GetMapping("/**/docs/{fileName}")
public ResponseEntity<byte[]> getDocument(@PathVariable String fileName) {
return ResponseEntity.ok(fileService.getContent(fileName));
}
// Matrix variables also work better now
@GetMapping("/search/{query}")
public List<SearchResult> search(
@PathVariable String query,
@MatrixVariable Map<String, String> filters) {
return searchService.execute(query, filters);
}
}
More powerful and expressive routing without the complexity of regex patterns.
Breaking Changes and Migration Path
What's Being Removed
| Deprecated Feature | Why | Alternative |
|---|---|---|
| javax.annotation.* | Migrated to Jakarta EE | Use jakarta.annotation.* |
| javax.inject.* | Migrated to Jakarta EE | Use jakarta.inject.* |
| spring-jcl module | Simplified logging | Use Apache Commons Logging directly |
| XML-based MVC config | Promotes modern practices | Use WebMvcConfigurer interfaces |
| JUnit 4 | Framework is EOL | Migrate to JUnit 5 |
| Jackson 2.x | Performance improvements | Upgrade to Jackson 3.x |
| Suffix pattern matching | Reduced complexity | Use explicit media types |
| Undertow servlet container | Resource optimization | Use Tomcat, Jetty, or Netty |
Step-by-Step Migration Checklist
- Step 1: Upgrade to Java 17+ (test with Java 21 LTS)
- Step 2: Update all javax.* imports to jakarta.*
- Step 3: Update your test framework to JUnit 5
- Step 4: Replace XML configurations with Java-based @Configuration classes
- Step 5: Update Jackson dependencies to version 3.x
- Step 6: Run your entire test suite
- Step 7: Test native image builds if you use GraalVM
- Step 8: Update documentation and deployment processes
Conclusion
Spring Boot 4.0 and Spring Framework 7 represent a significant leap forward in modern Java development. The focus on cloud-native patterns, improved developer experience, and stronger language features makes these releases essential upgrades.
The Java ecosystem isn't standing still, and neither should we. Spring Boot 4 and Spring Framework 7 are proof that Java continues to evolve with the industry's needs. Embrace the change, and your future applications will be faster, more maintainable, and easier to deploy in modern cloud environments.



