Java 21 and Java 25 in 2026: Differences and Migration Experience

java
Java 21 and Java 25 in 2026: Differences and Migration Experience

Hello.

As some of you already know, I have been working with Java and Spring Boot for more than 10 years, across different projects and teams in Latin America. During these years, I have worked with different Java versions and faced several upgrade and migration processes.

In 2026, I had the opportunity to work on an infrastructure composed of microservices developed with Java 21 and Java 25. As part of this environment, I also took part in migration processes from Java 21 to Java 25.

During this process, we encountered several challenges and some interesting situations that led me to investigate the differences between both versions and the evolution of Java over the last few years.

That is why I decided to write this article.

The idea is to share information that is useful to me and, at the same time, keep it available so I can refer back to it in the future. If it is also useful to someone going through a similar situation, even better.

The goal is not to create an official migration guide or explain absolutely every new feature in Java. I mainly want to share what I have been finding during this process and take the opportunity to review some of the most important differences between the versions.

Before We Start

Before getting directly into the Java 21 to Java 25 migration, I think it is interesting to take a look at how Java has evolved over the years.

That is why I prepared the following table, where we can see some of the main features and changes that appeared from Java 8 through Java 25.

Feature Java 8 Java 11 Java 17 Java 21 Java 25 (LTS)
Release 2014 2018 2021 2023 2025
Syntax and language Lambdas, Streams, Optional var for local variables, syntax and API improvements Records, Sealed Classes, Pattern Matching for instanceof Record Patterns, Pattern Matching for switch, String Templates in preview Flexible Constructor Bodies, Module Import Declarations, Compact Source Files
Garbage Collector Parallel GC; G1 available G1GC as the recommended option, ZGC experimental Improvements in G1 and ZGC Generational ZGC Generational Shenandoah and JVM improvements; Compact Object Headers
HTTP Client HttpURLConnection New standard HttpClient Improvements and optimizations Mature HTTP/2 and WebSockets support Improvements and optimizations
Structure / JVM Traditional JVM Introduction of the module system (Jigsaw) Modules consolidated Performance and memory improvements Compact Object Headers and other JVM improvements
Strings + operator or StringBuilder repeat(), isBlank() and other improvements Text Blocks String Templates in preview Further language and API improvements
Concurrency Traditional threads Internal improvements CompletableFuture improvements Virtual Threads and Structured Concurrency in preview Scoped Values and further concurrency improvements; Structured Concurrency remains in preview
Collections Traditional Collections API improvements API improvements Sequenced Collections API improvements
Pattern Matching Pattern Matching for instanceof Record Patterns and Pattern Matching for switch Further pattern-related improvements
Spring / ecosystem Ecosystem based on Java 8 Evolution of Spring and its dependencies Spring Boot 3 / Jakarta Spring Boot 3.x Spring Boot 4 / Spring Framework 7

Note: This table is a general reference to visualize the evolution of Java. It does not include every feature introduced in each version. Also, some features were initially introduced as preview or incubator features and evolved over several versions.

And this raises some interesting questions.

What Really Changes Between Java 21 and Java 25?

In my experience, the change from Java 21 to Java 25 did not mean that we had to learn a completely different language. Many of the fundamentals we use in Java 21 are still present in Java 25.

However, I did notice an important difference in terms of application performance in the environment I am working on.

I want to emphasize that this is a personal experience: I am not saying that every application moving from Java 21 to Java 25 will get exactly the same results. Performance depends on many factors: the application itself, workload, JVM, garbage collector, infrastructure, dependencies, and how the system is developed.

In our case, the change was significant enough to catch my attention.

There is another point that I consider even more important than performance.

Java has a characteristic that can work both in its favor and against it at the same time: a huge amount of code written years ago is still valid Java code.

We can find applications running on a modern JDK while continuing to be developed almost as if we were using Java 8.

And I consider this especially important when working with teams that include junior developers or when starting to incorporate Artificial Intelligence tools into the development process.

If we are working with a modern JDK, we should know the modern features available to us and evaluate when it makes sense to use them.

There is not much point in having JDK 25 and continuing to write all the code exactly the way we would have written it in Java 8 simply because "it compiles."

The compiler may accept old code, but that does not mean we are taking advantage of everything the platform has to offer.

What New Features Can We Take Advantage Of?

One thing I noticed during this process is that some important Java features are still not being used by every developer, often simply because they are not aware of their benefits.

One example is Records.

Records can be especially useful for representing DTOs when we need objects whose main purpose is to carry data and we want to reduce a large amount of boilerplate code.

For example:

public record UserResponse(
    Long id,
    String name,
    String email
) {}

This can be much simpler than creating a traditional class with fields, a constructor, getters, equals(), hashCode(), and toString().

Records also express a clear intention: we are working with a data type whose state is defined by its components.

However, there is something important to keep in mind: using Records does not automatically make a microservice more secure or guarantee OWASP compliance.

What we can achieve is a clearer data model and, because Record components are final, an immutability-oriented semantic that can help reduce certain design errors.

Security still depends on many other things: input validation, authorization, authentication, secret management, serialization, configuration, up-to-date dependencies, access controls, and so on.

Another interesting case is Sealed Classes, which became a final feature in Java 17.

Sealed Classes allow us to explicitly define which classes can extend or implement a particular type.

For example:

public sealed interface PaymentResult
    permits PaymentApproved, PaymentRejected {
}

This allows us to model the different states a particular hierarchy can have in a more explicit way.

In my opinion, these features should not simply be viewed as "new Java stuff." They are also part of a more modern way of designing code.

Java 25 does not only bring new features. It also continues refining features that appeared in previous versions.

What Can Break During the Migration?

This was one of the most interesting points of our migration.

We found a case related to Jackson that did not simply manifest itself as a compilation error.

In our case, when moving to Spring Boot 4, we encountered the migration from Jackson 2 to Jackson 3.

It is important to clarify this: the problem should not be attributed directly to "Java 25 breaking Jackson 2." The change is mainly related to the Spring Boot ecosystem upgrade and the transition from Jackson 2 to Jackson 3.

Spring Boot 4 uses Jackson 3 as its preferred JSON library, and Jackson 3 introduces new group IDs and packages. In general, packages that previously started with:

com.fasterxml.jackson

now use:

tools.jackson

An important exception is jackson-annotations, which keeps the com.fasterxml.jackson.annotation package.

Therefore, if our code directly uses Jackson classes, such as ObjectMapper, serializers, deserializers, or specific configuration classes, we need to review the imports and the way Jackson is configured.

The interesting thing about this problem is that the application can compile successfully and still fail when it starts processing real data.

That is why my recommendation during a migration is not to stop at:

"It compiled, so everything is fine."

We also need to test:

  • Input data.
  • Serialization.
  • Deserialization.
  • Endpoint responses.
  • Messages sent to and received from other microservices.
  • External integrations.
  • Cases involving optional or null data.
  • Integration tests.

In our case, we found objects that were not being transformed as expected during the data flow.

Therefore, when migrating an application from Java 21 / Spring Boot 3 to Java 25 / Spring Boot 4, I recommend paying special attention to any code that directly uses Jackson.

What Impact Do the New Versions Have on Spring Boot and Our Dependencies?

One of the benefits of moving to Java 25 is that we can also update the ecosystem and use modern versions of frameworks and libraries.

In our case, this is related to Spring Boot 4 and Spring Framework 7.

A migration is also a good opportunity to review third-party dependencies, remove libraries we no longer need, and update components that may be outdated.

But there is something I consider even more important: updating the JDK is not enough if the code continues using the same practices from years ago.

In my opinion, this requires continuous work with development teams.

We need to continuously promote these practices among developers, review Pull Requests, and use Code Reviews to detect when someone is writing code using features or patterns from much older Java versions when there is a modern and appropriate alternative.

Obviously, this does not mean that all old code is bad or that we have to use a new feature simply because it exists.

The question should be:

Does this modern feature actually improve the code we are writing?

If the answer is yes, we should consider it.

There is an analogy I like to use: a new car.

We can buy a car with many new features, but if the driver uses it exactly like their previous car and never uses any of the new functionality, we are wasting part of the investment.

Java is similar.

Having JDK 25 installed is not enough. We also need to learn how to use the capabilities it provides.

Are There Changes in JVM Behavior That We Should Keep in Mind?

As of August 9, 2026, during my experience with this migration, I have not found any JVM behavior changes that caused me a significant problem.

The main case we detected was related to the Spring Boot and Jackson ecosystem migration, as I mentioned earlier.

Other than that, so far I have not found another JVM behavior change that I consider important enough to highlight as a concrete problem in our migration.

However, I think it is important to clarify:

This is my experience with the projects and infrastructure I am currently working on.

It does not mean that a Java 21 to Java 25 migration will behave exactly the same way in another project.

Every application has its own characteristics, dependencies, workload, JVM configuration, infrastructure, and way of using Java.

So, once again, tests and integration testing are essential.

Is It Worth Migrating Immediately, or Is It Better to Wait?

In my opinion, yes, it is worth migrating, as long as the company and project context allows it.

But there is a reality that we often forget: a migration does not depend only on the development team.

In a large company, architecture, security, infrastructure, operations, QA, product, and different technical teams may all be involved.

Therefore, it is not always up to the "current squad" to decide when this type of migration should take place.

If the company's architecture team establishes that we need to migrate, my recommendation is to move forward and do it in an organized way.

Now, if we are working independently and have the possibility of creating a new product, I personally would recommend using a modern version such as Java 25, while always evaluating the compatibility of the ecosystem we are going to use.

I am particularly interested in Java 25 because it is an LTS version and because it allows us to work with a modern platform, with improvements in the language, JVM, concurrency, and performance.

Conclusion

After working on this migration, my main conclusion is that migrating from Java 21 to Java 25 should not be seen simply as changing a version in the pom.xml or the Dockerfile.

A real migration involves reviewing:

  • The JDK.
  • The JVM and its configuration.
  • Spring Boot.
  • Spring Framework.
  • Third-party libraries.
  • Serialization and deserialization.
  • Tests.
  • Integrations.
  • Our own code.
  • Development practices.

And above all, it means reviewing whether we are actually taking advantage of the capabilities offered by the new platform.

There is not much point in upgrading to Java 25 and then continuing to develop exactly as we did with Java 8.

For me, a migration should also be an opportunity to improve the code, review dependencies, remove unnecessary things, and teach the team about the new tools and capabilities available to us.

I will probably continue updating this article as I move forward with the migration and new cases appear.

For now, that is what I wanted to share.

If you are going through a similar migration and have found an interesting problem, feel free to leave it in the comments.

Thank you very much for reading.


Sources

Comments