Course topics

By WebNest Studio

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

Java
@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());
    }
}
Output
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

Java
@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
        }
    }
}
Output
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

Java
@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
Output
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

Java
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);
}
Output
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.