JPA can behave very differently depending on the exact circumstances under which it is used. Code that works in our local environment or in staging performs very poorly (or even flat out fails) when thrown against real-scale databases in production environments.

Debugging these JPA issues in production is pretty difficult - existing APMs don’t provide enough granular insights at the code level, and tracking every single place someone queried entities one by one instead of in bulk can be a grueling, time-consuming task.

Lightrun is a new approach to debugging in production. Using Lightrun’s Logs and Snapshots, you can now get debugger-level granularity in production without opening inbound ports, redeploying, restarting, or even stropping the running application.

In addition, instrumenting Lightrun Metrics at runtime allows you to track down persistence issues securely and in real-time. Want to see it in action? Check out our 2-minute tutorial for debugging JPA performance issues in production using Lightrun:

>> Debugging Spring Persistence and JPA Issues Using Lightrun

1. Introduction

In this brief tutorial, we'll focus on the different types of BootstrapMode for JPA repositories that Spring provides for altering the orchestration of their instantiation.

At startup, Spring Data scans for repositories and registers their bean definitions as singleton-scoped beans. During their initialization, repositories obtain an EntityManager immediately. Specifically, they get the JPA metamodel and validate declared queries.

JPA is bootstrapped synchronously by default. Consequently, the instantiation of repositories is blocked until the bootstrap process completes. As the number of repositories grows, the application could take a long time to start before it begins accepting requests.

2. Different Options for Bootstrapping Repositories

Let’s start by adding the spring-data-jpa dependency. As we are using Spring Boot, we’ll use the corresponding spring-boot-starter-data-jpa dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

We can tell Spring to use the default repository bootstrap behavior via a configuration property:

spring.data.jpa.repositories.bootstrap-mode=default

We can do the same by using annotations-based configuration:

@SpringBootApplication
@EnableJpaRepositories(bootstrapMode = BootstrapMode.DEFAULT)
public class Application {
    // ...
}

A third approach, restrained to a single test class, is to use the @DataJpaTest annotation:

@DataJpaTest(bootstrapMode = BootstrapMode.LAZY)
class BootstrapmodeLazyIntegrationTest {
    // ...
}

For the following examples, we'll use the @DataJpaTest annotation and explore the different repository bootstrap options.

2.1. Default

The default value for bootstrap mode will instantiate repositories eagerly. Hence, like any other Spring beans, their initialization will occur when injected.

Let's create a Todo entity:

@Entity
public class Todo {
    @Id
    private Long id;
    private String label;

    // standard setters and getters
}

Next, we'll need its associated repository. Let's create one that extends CrudRepository:

public interface TodoRepository extends CrudRepository<Todo, Long> {
}

Finally, let's add a test that uses our repository:

@DataJpaTest
class BootstrapmodeDefaultIntegrationTest {

    @Autowired
    private TodoRepository todoRepository;

    @Test
    void givenBootstrapmodeValueIsDefault_whenCreatingTodo_shouldSuccess() {
        Todo todo = new Todo("Something to be done");

        assertThat(todoRepository.save(todo)).hasNoNullFieldsOrProperties();
    }
}

After starting our test, let's check the logs where we'll find out how Spring bootstrapped our TodoRepository:

[2022-03-22 14:46:47,597]-[main] INFO  org.springframework.data.repository.config.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
[2022-03-22 14:46:47,737]-[main] TRACE org.springframework.data.repository.config.RepositoryConfigurationDelegate - Spring Data JPA - Registering repository: todoRepository - Interface: com.baeldung.boot.bootstrapmode.repository.TodoRepository - Factory: org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean
[2022-03-22 14:46:49,718]-[main] DEBUG org.springframework.data.repository.core.support.RepositoryFactorySupport - Initializing repository instance for com.baeldung.boot.bootstrapmode.repository.TodoRepository…
[2022-03-22 14:46:49,792]-[main] DEBUG org.springframework.data.repository.core.support.RepositoryFactorySupport - Finished creation of repository instance for com.baeldung.boot.bootstrapmode.repository.TodoRepository.
[2022-03-22 14:46:49,858]-[main] INFO  com.baeldung.boot.bootstrapmode.BootstrapmodeDefaultIntegrationTest - Started BootstrapmodeDefaultIntegrationTest in 3.547 seconds (JVM running for 4.877)

In our example, we initialize repositories early and make them available once the application has started.

2.2. Lazy

By using the lazy BootstrapMode for JPA repositories, Spring registers our repository's bean definition but does not instantiate it right away. Thus, using the lazy option, the first use triggers its initialization.

Let's modify our test and apply the lazy option to bootstrapMode:

@DataJpaTest(bootstrapMode = BootstrapMode.LAZY)

Then, let's launch our test with our fresh configuration, and check the corresponding logs:

[2022-03-22 15:09:01,360]-[main] INFO  org.springframework.data.repository.config.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in LAZY mode.
[2022-03-22 15:09:01,398]-[main] TRACE org.springframework.data.repository.config.RepositoryConfigurationDelegate - Spring Data JPA - Registering repository: todoRepository - Interface: com.baeldung.boot.bootstrapmode.repository.TodoRepository - Factory: org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean
[2022-03-22 15:09:01,971]-[main] INFO  com.baeldung.boot.bootstrapmode.BootstrapmodeLazyIntegrationTest - Started BootstrapmodeLazyIntegrationTest in 1.299 seconds (JVM running for 2.148)
[2022-03-22 15:09:01,976]-[main] DEBUG org.springframework.data.repository.config.RepositoryConfigurationDelegate$LazyRepositoryInjectionPointResolver - Creating lazy injection proxy for com.baeldung.boot.bootstrapmode.repository.TodoRepository…
[2022-03-22 15:09:02,588]-[main] DEBUG org.springframework.data.repository.core.support.RepositoryFactorySupport - Initializing repository instance for com.baeldung.boot.bootstrapmode.repository.TodoRepository…

We should pay attention to a couple of drawbacks here:

  • Spring might start accepting requests without having initialized repositories, thereby increasing latency when the first ones are handled.
  • Setting up BootstrapMode to lazy globally is error-prone. Spring will not validate queries and metadata contained in repositories that are not included in our tests.

We should use lazy bootstrapping only during development to avoid deploying an application in production with a potential initialization error. We can elegantly use Spring Profiles for this purpose.

2.3. Deferred

Deferred is the right option to use when bootstrapping JPA asynchronously. As a result, repositories don't wait for the EntityManagerFactory‘s initialization.

Let's declare an AsyncTaskExecutor in a configuration class by using ThreadPoolTaskExecutor – one of its Spring implementations – and override the submit method, which returns a Future:

@Bean
AsyncTaskExecutor delayedTaskExecutor() {
    return new ThreadPoolTaskExecutor() {
        @Override
        public <T> Future<T> submit(Callable<T> task) {
            return super.submit(() -> {
                Thread.sleep(5000);
                return task.call();
            });
        }
    };
}

Next, let's add an EntityManagerFactory bean to our configuration as shown in our Guide to JPA with Spring, and indicate that we want to use our asynchronous executor for background bootstrapping:

@Bean
LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, AsyncTaskExecutor delayedTaskExecutor) {
    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();

    factory.setPackagesToScan("com.baeldung.boot.bootstrapmode");
    factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
    factory.setDataSource(dataSource);
    factory.setBootstrapExecutor(delayedTaskExecutor);
    Map<String, Object> properties = new HashMap<>();
    properties.put("hibernate.hbm2ddl.auto", "create-drop");
    factory.setJpaPropertyMap(properties);
    return factory;
}

Finally, let's modify our test to enable deferred bootstrap mode:

@DataJpaTest(bootstrapMode = BootstrapMode.DEFERRED)

Let's launch our test again and check the logs:

[2022-03-23 10:31:16,513]-[main] INFO  org.springframework.data.repository.config.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFERRED mode.
[2022-03-23 10:31:16,543]-[main] TRACE org.springframework.data.repository.config.RepositoryConfigurationDelegate - Spring Data JPA - Registering repository: todoRepository - Interface: com.baeldung.boot.bootstrapmode.repository.TodoRepository - Factory: org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean
[2022-03-23 10:31:16,545]-[main] DEBUG org.springframework.data.repository.config.RepositoryConfigurationDelegate - Registering deferred repository initialization listener.
[2022-03-23 10:31:17,108]-[main] INFO  org.springframework.data.repository.config.DeferredRepositoryInitializationListener - Triggering deferred initialization of Spring Data repositories…
[2022-03-23 10:31:22,538]-[main] DEBUG org.springframework.data.repository.core.support.RepositoryFactorySupport - Initializing repository instance for com.baeldung.boot.bootstrapmode.repository.TodoRepository…
[2022-03-23 10:31:22,572]-[main] INFO  com.baeldung.boot.bootstrapmode.BootstrapmodeDeferredIntegrationTest - Started BootstrapmodeDeferredIntegrationTest in 6.769 seconds (JVM running for 7.519)

In this example, the application context bootstrap completion triggers the repositories' initialization. In short, Spring marks repositories as lazy and registers a DeferredRepositoryInitializationListener bean. When the ApplicationContext fires a ContextRefreshedEvent, it initializes all repositories.

Therefore, Spring Data initializes repositories and validates their included queries and metadata before the application startup.

3. Conclusion

In this article, we looked at various ways to initialize JPA repositories and in which cases to use them.
As usual, all the code samples used in this article can be found over on GitHub.

Persistence bottom
Get started with Spring Data JPA through the reference Learn Spring Data JPA course: >> CHECK OUT THE COURSE
Persistence footer banner
Comments are closed on this article!