News

Master Spring Data AOT in IntelliJ IDEA

Spring’s AOT engine has been around since the Spring Native days, but Spring Data never really benefited from it – until now. Repository infrastructure is one of the most dynamic parts of the framework, and for years it relied heavily on proxies, reflection, and queries generated at runtime.

That flexibility has a price: it slows startup and forces the system to do considerable runtime processing just to interpret something as simple as findPersonByNameLike.

With the latest Spring Data release, repositories finally get proper AOT support. Method queries can now be pre-generated during the build process, so you don’t have to wait until runtime.

And here’s where IntelliJ IDEA 2025.3 comes in: you can now inspect, navigate, and debug these AOT-generated repository classes in the IDE. This article focuses on the developer experience, exploring how to configure build tools and how IntelliJ IDEA makes the development process faster, easier, and less error-prone.

AOT repository internals overview

Enabling AOT is an easy task. You can find a good manual in the official documentation for both:

After running the build, by default, AOT-generated sources appear in:

  • build/generated/aotSources for Gradle
  • target/spring-aot/main/classes/ if you use Maven

For IntelliJ IDEA, two artifacts matter most:

  • The generated source code
  • The JSON metadata describing repository methods and their queries

To illustrate, let’s assume our application exposes quotes, and we need a method to fetch quotes by author:

List<Quote> findAllByAuthor(String author);

Depending on whether you use Spring Data JPA or JDBC, the generated implementation varies.

Example: Spring Data JPA

For JPA repositories, IntelliJ IDEA shows the generated JPQL query above the method signature.

Clicking the icon takes you straight to the AOT-generated implementation:

public List<Quote> findAllByAuthor(String author) {
  String queryString = "SELECT q FROM Quote q WHERE q.author = :author";
  Query query = this.entityManager.createQuery(queryString);
  query.setParameter("author", author);
  
  return (List<Quote>) query.getResultList();
}

This is the actual code that will be executed at runtime when AOT mode is enabled.

Example: Spring Data JDBC

For Spring Data JDBC, the picture is a bit different. We can see pure SQL and a list of fields selected for mapping to the Java object. This is the repository code with the query above the method: 

And this is the generated code:

public List<Quote> findAllByAuthor(String author) {
  Criteria criteria = Criteria.where("author").is(author);
  StatementFactory.SelectionBuilder builder = getStatementFactory().select(Quote.class).filter(criteria);

  RowMapper rowMapper = getRowMapperFactory().create(Quote.class);
  List result = (List) builder.executeWith((sql, paramSource) -> getJdbcOperations().query(sql, paramSource, new RowMapperResultSetExtractor<>(rowMapper)));
  return (List<Quote>) convertMany(result, Quote.class);
}

How IntelliJ IDEA displays queries

IntelliJ IDEA treats the AOT output as part of the project’s source set. This provides the following:

  • Direct navigation into generated methods
  • Code highlighting and analysis
  • Linking compiled classes to their generated sources

Spring Data’s AOT process also creates JSON metadata for every repository method. These files are a part of the project’s resources, and they provide the information about the actual JPQL/SQL query: 

For JPA:

{
  "name": "findAllByAuthor",
  "signature": "public abstract java.util.List<org.test.demo2gradleaot.hello.Quote> org.test.demo2gradleaot.hello.QuoteRepository.findAllByAuthor(java.lang.String)",
  "query": {
    "query": "SELECT q FROM Quote q WHERE q.author = :author"
  }
}

And for JDBC:

{
  "name": "findAllByAuthor",
  "signature": "public abstract java.util.List<com.jetbrains.test.boot4.server.quote.Quote> com.jetbrains.test.boot4.server.quote.QuoteRepository.findAllByAuthor(java.lang.String)",
  "query": {
    "query": "SELECT \"quote\".\"id\" AS \"id\", \"quote\".\"text\" AS \"text\", \"quote\".\"author\" AS \"author\", \"quote\".\"source\" AS \"source\" FROM \"quote\" WHERE \"quote\".\"author\" = :author"
  }
}

If the generated query needs refinement, you can use the Inline Query action to insert it into a @Query annotation in the Spring Data repository code. 

The annotated query will then be used as is during AOT generation.

Most importantly, AOT-generated repository code is fully debuggable. You can place breakpoints directly inside the generated method and observe the exact query being executed – without navigating proxies or JDBC driver internals. 

Running applications with AOT

By default, Spring Boot applications do not load AOT-generated classes. Neither ./gradlew bootRun nor ./mvnw spring-boot:run nor clicking on the green triangle icon in IntelliJ IDEA will load AOT-generated classes into the JVM. You need to explicitly instruct the runner to use them. To do this, you pass the -Dspring.aot.enabled=true flag for the application’s JVM. 

Let’s have a look at the required setup for both Gradle and Maven.

Gradle

The simplest way to pass the spring.aot.enabled flag is to modify the build.gradle.kts and add the following lines:

tasks.named<BootRun>("bootRun") {
    if (project.hasProperty("aot")) {
        jvmArgs("-Dspring.aot.enabled=true")
        systemProperty("spring.profiles.active", "aot")
    }
}

This will make Gradle search for the project property aot, bind its value to the system property spring.aot.enabled for the running application, and load a profile with DB connection properties. To run the application with AOT code, use the following command:

./gradlew bootRun -Paot

Maven

For Maven, you need to:

  • Execute the package lifecycle phase to generate AOT classes before running the application.
  • Pass the spring.aot.enabled flag to the app JVM.

It is usually a good idea to make a separate Maven profile to run the application with AOT classes:   

<profile>
    <id>aot</id>
    <properties>
        <spring-boot.run.profiles>aot,localdb</spring-boot.run.profiles>
        <spring-boot.run.jvmArguments>-Dspring.aot.enabled=true</spring-boot.run.jvmArguments>
    </properties>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>process-aot</id>
                        <goals>
                            <goal>process-aot</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</profile>

Now you can run the application using:

./mvnw -Paot package spring-boot:run

💡 Note: Spring Boot’s automatic Docker Compose integration doesn’t currently work when the application is launched in AOT mode. If your project uses Docker Compose integration, start required services manually and configure the proper connection properties (for example, via a dedicated Spring profile). Please note the `localdb` profile string in the provided settings. 

Debugging AOT code with IntelliJ IDEA

IntelliJ IDEA can run and debug an application that was started by Gradle or Maven. Just create a proper run configuration, and you’re good to go. 

For Gradle:

For Maven:

IntelliJ IDEA’s native build system still has limitations when dealing with AOT. So, if you’re aiming to debug AOT repositories, we advise you to build the code using the external build system, then run the application with IntelliJ IDEA, and pass system variables and profile values as required. The run configuration will look like this (assuming you’re using Maven):

Please note the following in the screenshot above:

  • The Before launch task
  • The JVM Args field with the spring.aot.enabled variable

With either of these run configurations, you’ll be able to start the application and debug AOT repositories, while also having access to Spring Debugger features.

Seeing a real query string and ongoing transaction – isn’t it exciting? 

Limitations

They say that there’s no such thing as a free lunch. And that’s certainly true when working with AOT, which is why we must bear several important points in mind.

Spring Data JDBC dialect requirement

Spring Data JDBC currently requires a dialect bean for AOT mode:

@Configuration
class AotConfiguration {
    @Bean
    JdbcDialect dialect() {
        return JdbcPostgresDialect.INSTANCE;
    }
}

This requirement will probably be removed in subsequent Spring Boot 4 updates (see issue #47781). Now this workaround is mentioned in the documentation, but it’s easy to overlook. 

DevTools incompatibility

If you see the following strange error during startup:

java.lang.IllegalAccessError: failed to access class org.springframework.boot.autoconfigure.AutoConfigurationPackages$BasePackages 
from class org.springframework.boot.autoconfigure.AutoConfigurationPackages__BeanDefinitions$BasePackages (org.springframework.boot.autoconfigure.AutoConfigurationPackages$BasePackages 
is in unnamed module of loader 'app'; org.springframework.boot.autoconfigure.AutoConfigurationPackages__BeanDefinitions$BasePackages 
is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @2b4a127a)

There’s a good chance that DevTools is on the classpath. At the moment, AOT and DevTools are not fully compatible, so just disable DevTools, and the application will start normally.  

Conclusion

Spring Data AOT repositories introduce several meaningful improvements:

  • Faster application startup 
  • Reduced memory usage
  • Better native image performance
  • And most importantly: real visibility into behavior that used to be hidden behind proxies and reflection

With IntelliJ IDEA 2025.3, you can now see the exact SQL or JPQL queries used by your application, navigate directly into the generated repository methods, and debug them just like any other code. This removes much of the guesswork associated with Spring Data behavior and lets you focus on what matters most – building clean, reliable business logic.

image description