0%

Spring Boot Startup Execution Order, and Place for Database Data Initialization

April 25, 2026

Springboot

1.

The Problem: Where Do We Initialize Database Data?

A common need in Spring Boot applications is seeding or resetting data at startup — for example, setting all task statuses back to PENDING after a server restart, or pre-populating a lookup table.

The question is: which lifecycle hook is the right place to do this?

We have two main candidates: @PostConstruct and ApplicationRunner. Choosing the wrong one can cause runtime errors that are hard to debug — a @Repository or EntityManager that looks wired but isn't fully ready yet will throw a NullPointerException or a JPA initialization error silently during startup.

This article walks through the Spring Boot startup sequence to show exactly why ApplicationRunner is the safe choice for database work, and when @PostConstruct is appropriate instead.


2.

Full Sequence

PlantUML diagram

The embedded server (Tomcat/Netty) will open its ports only after all of the ApplicationRunners have returned.

This is enforced inside SpringApplication.run():

PlantUML diagram

3.

ApplicationRunner

3.1.

Imports

ApplicationRunner comes from Spring Boot — no extra dependency needed beyond spring-boot-starter:

import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.core.annotation.Order
import org.springframework.core.Ordered
import org.springframework.stereotype.Component

3.2.

Example

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class TableInitializer(
    private val myRepository: MyRepository
) : ApplicationRunner {

    override fun run(args: ApplicationArguments) {
        myRepository.reinitStatus()
    }
}

Use @Order(Ordered.HIGHEST_PRECEDENCE) when we have multiple runners and need this one to go first.

3.3.

Why Repositories Are Safe Inside ApplicationRunner

"Safe" here means the JPA infrastructure is fully initialized — EntityManagerFactory, the DataSource connection pool, and all @Repository proxies are ready to accept calls.

Attempting the same DB work inside @PostConstruct is risky because that hook fires during bean creation, before the full context refresh completes: JPA proxies may exist as objects but their underlying infrastructure may not be wired yet, leading to LazyInitializationException or NullPointerException at runtime.

By the time ApplicationRunner.run() is called, the full ApplicationContext is refreshed:

InfrastructureStatus
EntityManagerFactoryInitialized
DataSource connection poolOpen
@Repository proxiesFully wired
Transaction managerReady

4.

@PostConstruct

4.1.

Imports

@PostConstruct is part of the Jakarta EE annotations, available in Spring Boot via spring-boot-starter (bundled through jakarta.annotation-api):

import jakarta.annotation.PostConstruct
import org.springframework.stereotype.Component

Remark. For older Spring Boot 2.x projects still on Java EE, the import is javax.annotation.PostConstruct instead.

4.2.

Example

@Component
class CacheWarmer(
    private val configProperties: ConfigProperties
) {
    private lateinit var allowedRoles: Set<String>

    @PostConstruct
    fun init() {
        // runs after this bean is fully wired, safe for in-memory work
        allowedRoles = configProperties.roles.toSet()
    }
}

5.

@PostConstruct vs ApplicationRunner

@PostConstructApplicationRunner
TimingDuring bean creationAfter full context startup
Repositories safe to useRisky (JPA infra may not be ready)Always safe
Order control@DependsOn@Order(n)
Server accepting requestsNot yetNot yet (both run before)
Recommended for DB initNoYes

6.

Rule of Thumb

  • Use @PostConstruct for pure in-memory initialization;

  • Use ApplicationRunner for anything touching the database or other fully-wired Spring infrastructure.