Let's get started with a Microservice Architecture with Spring Cloud:
Introduction to Spring Data AOT Repositories
Last updated: June 8, 2026
1. Overview
In the last few years, there has been a lot of effort in the Java ecosystem to improve JVM performance. We have seen the JIT compiler, then the Ahead of Time (AoT) Compilation, and lately the Native Images.
Popular Java frameworks, like Micronaut and Spring, have moved in the same direction to achieve better performance through compilation improvements. Spring offers both Ahead of Time and Native Image support. However, in such big frameworks, there is still a lot of use of Reflection, so there is still plenty of room for improvement. The latest feature, to be introduced in Spring Boot 4, is an AOT optimization for Spring Data, with the AOT Repositories.
In this article, we’re going to see what the new feature of Spring Data AOT Repositories brings in. Then, we’ll demonstrate how it works. In the end, we’re going to compare its usage with the current alternatives, using Spring Boot 3 with and without AOT compilation.
2. Dependencies
Let’s start by defining the minimum dependencies possible, for demonstrating the Spring Data AOT Repositories. We can only keep the spring-boot-starter-web and spring-boot-starter-data-jpa. The first is for offering an endpoint to verify that the repository actually works, and the latter contains everything needed for Spring to interact with a database:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>4.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>4.0.5</version>
</dependency>
Spring Boot major version 4 is only available in early stages. We’re still going to use the stable 4.0.5 for our demonstration.
3. Spring Data Repository
Moving on, let’s look at how repository implementations have evolved from being runtime proxies to the build-time generated code with Spring Data AOT Repositories. We’ll use a very simple and popular example of a User entity and the respective UserRepository. This will let us compare behavior and performance across the three options of repositories without AOT, repositories with AOT (but before AOT Repositories), and AOT Repositories.
3.1. The Domain Model
We’ll use a short User entity, with just three fields, for demonstration purposes:
@Entity
@Table(name = "USERS")
public class User {
@Id
@GeneratedValue
private Long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
// constructors, setters, getters, equals, etc.
}
The entity class only contains an id, a firstName, and a lastName field. We can use all of the Spring Data and JPA annotations we’re familiar with. Then we’ll create the related UserRepository:
public interface UserRepository extends Repository<User, Long> {
User save(User user);
@Transactional(readOnly = true)
List<User> findAll();
List<User> findAllById(Iterable<Long> longs);
@Query(value = "SELECT * FROM users", nativeQuery = true)
List<User> nativeQueryFindAllUsers();
@Query(value = "SELECT u FROM User u")
List<User> queryFindAllUsers();
}
First, we’ll include three methods that Spring Data can translate into queries, as normal. On top of that, we’ll add two more methods annotated with @Query, one native and one non-native. This is so we see how Spring Data AOT Repository handles the different cases.
Second, we extend the Repository Interface to be more generic. The dependency we’ve included earlier will translate it to JpaRepository.
3.2. Spring Data Repositories Without AOT
Before Spring AOT optimizations and Spring Data AOT Repositories, we would compile the code by using the mvn clean install command. This would convert the Java file to a class, without any changes, except for indentation.
This means that the implementation is not generated. As we know, everything happens at runtime:
- Spring finds the Repository interfaces
- Passes them to the JpaRepositoryFactory
- The JpaRepositoryFactory builds the SimpleJpaRepository instances
- Then wraps each instance with a dynamic proxy that routes calls from your interface methods, like findAll(), to the corresponding methods on SimpleJpaRepository, or to the Query and JPQL implementations, or to any custom implementations
Everything is being built at runtime, based on reflection. This means that startup time and memory are being affected.
3.3. Repositories With AOT, Before Spring Data AOT Repositories
Spring 6 came with Ahead of Time Optimizations. To enable this feature, we need to:
- Compile the code, including the spring-boot:process-aot task, or set up the build plugin to enable AOT compilation by default
- Execute the application setting the spring.aot.enabled property to true
Regarding the Spring Data Repositories, the result is the same as before. This means that we get the same class, and then Spring uses the SimpleJpaRepository, reflection, and proxies at runtime to make everything happen. The difference is that the AOT compilation creates an additional class, the UserRepository__BeanDefinitions class, with some reflection metadata and other info precomputed at build time. This improves startup time.
3.4. Spring Data AOT Repositories
The new feature, coming with Spring Data 4, is the Spring Data AOT Repositories. With this feature, Spring is shifting all the Repository preparations that are done at application startup to build time.
Let’s see how this works, on a high level. Spring turns your Repository query methods into actual source code by relying on the store-specific nature of the repository. The methods that are already implemented are used directly. For instance, the save() method is implemented in SimpleJpaRepository, in our example. Spring doesn’t need to generate any code for such methods. However, the remaining methods will be implemented inside the UserRepositoryImpl__AotRepository class.
The way to compile the code using AOT remains the same as in the previous Spring version. We can find the generated classes in our target/classes folder:
@Generated
public class UserRepositoryImpl__AotRepository extends AotRepositoryFragmentSupport {
private final RepositoryFactoryBeanSupport.FragmentCreationContext context;
private final EntityManager entityManager;
public List<User> nativeQueryFindAllUsers() {
String var1 = "SELECT * FROM users";
Query var2 = this.entityManager.createNativeQuery(var1, User.class);
return var2.getResultList();
}
public List<User> queryFindAllUsers() {
String var1 = "SELECT u FROM User u";
Query var2 = this.entityManager.createQuery(var1);
return var2.getResultList();
}
}
Methods annotated with @Query contain the same code we would write if we weren’t using Spring Data. The UserRepository__BeanDefinitions class is present, similar to previous versions. The last source generated by this new feature is a JSON file with hints for native-image, UserRepository.json.
This way Spring generates a concrete repository implementation at build time, eliminating the proxy and most runtime reflection. This improves both the startup time and memory.
To enable usage of Spring Data AOT Repositories, we need to execute the application setting the spring.aot.repositories.enabled property to true:
mvn spring-boot:run -Dspring.aot.enabled=true -Dspring.aot.repositories.enabled=true
4. Performance Implications
Spring Data AOT Repositories are expected to improve application performance. Startup time should be reduced, and some memory consumption as well, as advertised in the relevant Spring Blog article:
Next, we’ll compare the performance of the three implementations we discussed so far:
- Spring before AOT, using the java -jar spring-data-jpa-not-aot/target/spring-data-jpa-not-aot-0.0.1-SNAPSHOT.jar command
- Spring with AOT improvements, using the java -Dspring.aot.enabled=true -jar spring-data-jpa-aot/target/spring-data-jpa-aot-0.0.1-SNAPSHOT.jar command
- Spring Data AOT Repositories, using the java -Dspring.aot.enabled=true -Dspring.aot.repositories.enabled=true -jar spring-data-jpa-aot-repository/target/spring-data-jpa-aot-repository-0.0.1-SNAPSHOT.jar command
We’ll use five entity classes and repositories to try to make the changes more visible. And we’ll focus on the startup time and memory consumption.
4.1. Build Times Comparison
As already explained, the new AOT Repositories feature performs additional work at build time to construct the Repositories. So, this means that we expect slower build times when we enable this feature:
- repositories without AOT: Total time: 11.076 s
- repositories with AOT: Total time: 17.166 s
- AOT Repositories: Total time: 25.390 s
With Spring Data AOT Repositories, if we have syntax errors in JPA methods, we see compile-time errors. This makes sense, since the transformation of JPA methods to SQL Statements now happens at compile rather than runtime.
4.2. Startup Times Comparison
Next, we’ll execute a script that starts each service, and at the same time, it starts trying to hit an endpoint that uses a repository. This way, we can monitor how long it took for the service to start, since the server and the repositories are ready for use.
For the repositories without AOT, the script output was:
==== RESULTS ====
time elapsed 10148 millis
Process Specific Memory/CPU (RSS KB / CPU Time): 289840 00:00:28
For the repositories with AOT:
==== RESULTS ====
time elapsed 9885 millis
Process Specific Memory/CPU (RSS KB / CPU Time): 291104 00:00:25
And for Spring Data AOT Repositories:
==== RESULTS ====
time elapsed 8745 millis
Process Specific Memory/CPU (RSS KB / CPU Time): 292864 00:00:23
This makes perfect sense, since we know that AOT and AOT Repositories target the improvement of startup times. And this is clear even for small projects like ours.
The memory improvement that is advertised by Spring is not shown here, but it makes sense for our small project.
Note, when starting the application without AOT, we should notice a log saying: Starting Application. But when using AOT, it should say Starting AOT-processed Application.
4.3. Comparison Under Load
Our last test includes a script that sends some traffic to each implementation and compares the performance.
For the repositories without AOT, the script output was:
==== RESULTS ====
Total requests: 6688
Success (2xx): 6688
...
Avg duration (measured): 51.7629ms
...
Max memory utilised: 331888
For the repositories with AOT:
==== RESULTS ====
Total requests: 7664
Success (2xx): 7664
...
Avg duration (measured): 43.934ms
...
Max memory utilised: 334000
And for Spring Data AOT Repositories:
==== RESULTS ====
Total requests: 6673
Success (2xx): 6673
...
Avg duration (measured): 49.0766ms
...
Max memory utilised: 337948
The result doesn’t show the memory improvement that Spring advertises, but this could be impacted by the project sizes and the data store.
5. Conclusion
In this article, we explored the new feature of Spring Boot 4, Spring Data AOT Repositories. We used a simple UserRepository class to demonstrate how Spring Data Repository worked before and what changes exactly this new feature introduced. Finally, we compared the performance implications of using the Spring Data AOT Repositories.
As always, the source code of the examples can be found over on GitHub.
















