Spring Boot Tutorial
Application Startup: Runners and Lifecycle Events
Many applications need to do something when they start — load reference data, warm a cache, validate configuration, print a summary, or start a background consumer — and something when they stop, like flushing buffers or finishing in-flight work. Spring Boot offers several hooks for this, each running at a different moment of the lifecycle.
This lesson walks through the startup sequence of SpringApplication, CommandLineRunner and ApplicationRunner, the lifecycle events you can listen to, SmartLifecycle for components that start and stop, graceful shutdown, and customising SpringApplication itself.
The Startup Sequence
When SpringApplication.run() executes, events are published in a fixed order: ApplicationStartingEvent → ApplicationEnvironmentPreparedEvent (configuration loaded) → ApplicationContextInitializedEvent → ApplicationPreparedEvent (bean definitions loaded) → context refresh (beans created, @PostConstruct runs, web server starts) → ApplicationStartedEvent → runners execute → ApplicationReadyEvent (the application is ready to serve). If anything fails, ApplicationFailedEvent is published instead.
CommandLineRunner and ApplicationRunner
Beans implementing these interfaces run once, after the context has started and before the application is marked ready. CommandLineRunner.run(String... args) gets raw arguments; ApplicationRunner.run(ApplicationArguments args) gets parsed options (--mode=import) and non-option arguments. Order several runners with @Order. Exceptions thrown from a runner stop the application — useful for fail-fast checks. Runners are ideal for CLI-style Spring Boot tools and one-off data setup.
Listening to Events
Use @EventListener(ApplicationReadyEvent.class) for work that should only happen once the app is fully ready — for example registering with an external system or logging a startup summary. Early events (starting, environment prepared) fire before the context exists, so their listeners must be registered via SpringApplication.addListeners() or META-INF/spring.factories, not as beans.
SmartLifecycle for Start/Stop Components
Components that own a background thread or connection — a message poller, a scheduler, a socket server — should implement SmartLifecycle. Spring calls start() after all singletons are created and stop() during shutdown, in an order controlled by getPhase(). Combined with graceful shutdown, this lets in-flight work complete before the JVM exits.
Graceful Shutdown
Spring Boot enables graceful shutdown by default for embedded web servers: on SIGTERM, the server stops accepting new requests and waits for active ones to finish, up to spring.lifecycle.timeout-per-shutdown-phase (30s by default). Then lifecycle beans stop, @PreDestroy methods run and the context closes. This is essential for zero-downtime deployments on Kubernetes.
Examples
CommandLineRunner and ApplicationRunner with ordering
@Component
@Order(1)
public class SeedDataRunner implements CommandLineRunner {
private final CourseRepository courses;
public SeedDataRunner(CourseRepository courses) {
this.courses = courses;
}
@Override
public void run(String... args) {
if (courses.count() == 0) {
courses.saveAll(List.of(new Course("java-core", "Java - Core"),
new Course("spring-boot", "Spring Boot")));
System.out.println("Seeded 2 courses");
}
}
}
@Component
@Order(2)
public class ImportRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
if (args.containsOption("import")) {
String file = args.getOptionValues("import").get(0);
System.out.println("Importing students from " + file);
}
System.out.println("Non-option args: " + args.getNonOptionArgs());
}
}
java -jar app.jar --import=students.csv dry-run
Seeded 2 courses
Importing students from students.csv
Non-option args: [dry-run]
Started WebnestAppApplication in 2.1 seconds
Reacting to ApplicationReadyEvent and failing fast on bad configuration
@Component
public class StartupReporter {
private static final Logger log = LoggerFactory.getLogger(StartupReporter.class);
private final Environment env;
public StartupReporter(Environment env) {
this.env = env;
}
@EventListener(ApplicationReadyEvent.class)
public void ready(ApplicationReadyEvent event) {
log.info("Ready on port {} with profiles {} in {} ms",
env.getProperty("local.server.port"),
Arrays.toString(env.getActiveProfiles()),
event.getTimeTaken().toMillis());
}
}
@Component
class RequiredConfigCheck implements ApplicationRunner {
@Value("${payments.api-key:}")
private String apiKey;
@Override
public void run(ApplicationArguments args) {
if (apiKey.isBlank()) {
throw new IllegalStateException("payments.api-key must be set"); // stops the app
}
}
}
INFO StartupReporter : Ready on port 8080 with profiles [dev] in 2143 ms
(without the key)
ERROR SpringApplication : Application run failed
java.lang.IllegalStateException: payments.api-key must be set
A SmartLifecycle background worker with graceful shutdown
@Component
public class OutboxPublisher implements SmartLifecycle {
private static final Logger log = LoggerFactory.getLogger(OutboxPublisher.class);
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private volatile boolean running;
@Override
public void start() {
running = true;
executor.scheduleWithFixedDelay(this::publishPending, 0, 1, TimeUnit.SECONDS);
log.info("Outbox publisher started");
}
@Override
public void stop() {
running = false;
executor.shutdown();
try {
executor.awaitTermination(10, TimeUnit.SECONDS); // finish the current batch
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("Outbox publisher stopped");
}
@Override
public boolean isRunning() {
return running;
}
private void publishPending() { /* read outbox table, send events */ }
}
# application.yml
server:
shutdown: graceful # default in Spring Boot, shown for clarity
spring:
lifecycle:
timeout-per-shutdown-phase: 20s
INFO Outbox publisher started
(Ctrl+C / SIGTERM)
INFO GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
INFO GracefulShutdown : Graceful shutdown complete
INFO Outbox publisher stopped
Customising SpringApplication
public static void main(String[] args) {
SpringApplication app = new SpringApplication(WebnestAppApplication.class);
app.setBannerMode(Banner.Mode.OFF);
app.setDefaultProperties(Map.of("server.port", "8081"));
app.addListeners((ApplicationListener<ApplicationEnvironmentPreparedEvent>) e ->
System.out.println("Environment ready, profiles = "
+ Arrays.toString(e.getEnvironment().getActiveProfiles())));
app.run(args);
}
Environment ready, profiles = []
Tomcat started on port 8081 (http)
Common Mistakes
- Doing slow work (large imports) in @PostConstruct, delaying startup and blocking health checks; use a runner or ApplicationReadyEvent and consider async execution.
- Swallowing exceptions in startup checks, so a misconfigured application starts "successfully" and fails later.
- Registering early lifecycle event listeners as @Component beans; they fire before the context exists and never reach the bean.
- Starting threads in constructors without stopping them, preventing clean shutdown.
- Setting a Kubernetes termination grace period shorter than the Spring shutdown timeout, so pods are killed mid-request.
Key Points to Remember
- SpringApplication publishes lifecycle events from ApplicationStartingEvent to ApplicationReadyEvent.
- CommandLineRunner and ApplicationRunner run once after startup; order them with @Order.
- Use @EventListener(ApplicationReadyEvent.class) for work that needs a fully ready app.
- SmartLifecycle manages components with start/stop behaviour and phases.
- Graceful shutdown lets active requests finish; tune spring.lifecycle.timeout-per-shutdown-phase.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.