Skip to content

Spring Boot · Core

The Spring Boot Application Lifecycle, Start to Shutdown

6 min read Updated 2026-06-26 Share:

Practice Application Lifecycle interview questions

Why the lifecycle matters

"Where do I put startup code?" and "why did my bean fail before the app even served a request?" are everyday Spring Boot questions, and both are answered by understanding the lifecycle. Knowing the order of operations tells you exactly which hook to use — and why fail-fast startup is a feature, not a bug. This article walks the whole journey.

What SpringApplication.run actually does

public static void main(String[] args) {
    SpringApplication.run(App.class, args);
    // 1. Create SpringApplication, infer app type (servlet / reactive / none)
    // 2. Run ApplicationContextInitializers, fire ApplicationStartingEvent
    // 3. Prepare the Environment (load properties, activate profiles)
    // 4. Create the ApplicationContext
    // 5. refresh() — process @Configuration, run auto-config, instantiate beans
    // 6. Start the embedded web server (web apps)
    // 7. Call ApplicationRunner / CommandLineRunner beans
    // 8. Fire ApplicationReadyEvent — now serving traffic
}

The centerpiece is step 5, refresh(). Everything else sets up the environment around it or reacts to its completion.

Inside refresh()

refresh() (defined in AbstractApplicationContext) turns bean definitions into wired singletons in a fixed order:

1. prepareBeanFactory
2. invokeBeanFactoryPostProcessors
     → ConfigurationClassPostProcessor parses @Configuration, evaluates @Conditional,
       registers every bean definition (auto-configuration materializes here)
3. registerBeanPostProcessors           (AOP, @Autowired, @ConfigurationProperties binding)
4. initMessageSource / event multicaster
5. onRefresh                            (create the embedded web server)
6. registerListeners
7. finishBeanFactoryInitialization      (instantiate all non-lazy singletons)
8. finishRefresh                        (publish ContextRefreshedEvent, start Lifecycle beans)

The distinction worth memorizing:

  • BeanFactoryPostProcessor runs in step 2 — it edits bean definitions before any bean exists.
  • BeanPostProcessor runs in step 7 — it wraps each bean as it is created (this is how AOP proxies and @Autowired injection happen).

The bean lifecycle and its callbacks

Each bean travels: instantiate → inject dependencies → initialize → use → destroy. You hook the ends:

@Component
public class CacheWarmer {
    @PostConstruct  // after dependencies are injected
    void init() { /* warm caches, open connections, validate config */ }

    @PreDestroy     // on orderly shutdown, before destruction
    void cleanup() { /* flush, close pools */ }
}

Preference order: @PostConstruct/@PreDestroy (cleanest) → InitializingBean/DisposableBean (couples to Spring) → @Bean(initMethod, destroyMethod) (for classes you can't annotate). Note @PreDestroy only runs on an orderly shutdown — never on kill -9.

Ordering beans

Constructor injection implies order automatically. When two beans aren't directly related but order still matters, be explicit:

@Component
@DependsOn("dataSource")   // run after dataSource even without a field reference
public class FlywayMigrator { }

For collections injected as a List<T>, order the elements with @Order instead.

Running code at startup: runners

Both runner interfaces execute once, after the context is ready but before run() returns:

@Component
class StartupTasks implements CommandLineRunner {
    public void run(String... args) { /* raw String[] */ }
}

@Component
class ParsedStartup implements ApplicationRunner {
    public void run(ApplicationArguments args) {
        boolean verbose = args.containsOption("verbose"); // --verbose
        List<String> files = args.getNonOptionArgs();
    }
}

Use ApplicationRunner for parsed --option access, CommandLineRunner for the raw array, and @Order to sequence several.

@PostConstruct vs a runner

A common interview distinction: @PostConstruct runs during one bean's initialization, while the context may still be assembling. A runner runs once for the whole app, after everything is wired. Use @PostConstruct for a bean's own setup; use a runner for cross-bean work like seeding data or kicking off jobs.

Lifecycle events

Spring Boot publishes a sequence of events you can listen to:

@Component
class LifecycleListener {
    @EventListener
    void onReady(ApplicationReadyEvent e) { /* app is live — register with discovery, etc. */ }

    @EventListener
    void onFailed(ApplicationFailedEvent e) { /* startup failed — alert and clean up */ }
}

Ordered: ApplicationStartingEvent → ApplicationEnvironmentPreparedEvent → ApplicationContextInitializedEvent → ApplicationPreparedEvent → (refresh) → ApplicationStartedEvent → AvailabilityChangeEvent(READY) → ApplicationReadyEvent, with ApplicationFailedEvent on error. The earliest events fire before the context exists, so register for those via META-INF/spring.factories, not @EventListener.

SmartLifecycle for long-running components

@PostConstruct runs mid-creation; sometimes you need to start something only once the entire context is up — a Kafka consumer, a poller, a socket server. That's SmartLifecycle:

@Component
class MessageConsumer implements SmartLifecycle {
    private volatile boolean running;
    public void start() { running = true;  /* begin consuming */ }
    public void stop()  { running = false; /* drain & disconnect */ }
    public boolean isRunning() { return running; }
    public int getPhase() { return 100; }       // higher = start later, stop earlier
    public boolean isAutoStartup() { return true; }
}

start() runs at the end of refresh, with phases giving deterministic ordering across lifecycle beans.

Customizing startup before the context exists

Some settings must be applied before the context is created — banner, app type, very early listeners:

SpringApplication app = new SpringApplication(App.class);
app.setBannerMode(Banner.Mode.OFF);
app.setWebApplicationType(WebApplicationType.NONE);   // e.g. a batch job
app.setAdditionalProfiles("metrics");
app.addListeners(new MyStartupListener());
app.run(args);

Failing fast is a feature

If any non-lazy singleton throws during creation, refresh() aborts, the context closes, ApplicationFailedEvent fires, and the JVM exits non-zero — the app never serves traffic in a broken state:

@Component
class ConfigChecker {
    @PostConstruct
    void validate() {
        if (invariantBroken())
            throw new IllegalStateException("Refusing to start: invalid configuration");
    }
}

A registered FailureAnalyzer can turn the exception into a friendly "Description / Action" block. Throwing early to enforce invariants is good practice — a misconfigured app should refuse to start.

This is also why eager initialization is the default. spring.main.lazy-initialization=true speeds startup but defers failures to first request, sacrificing fail-fast. Make individual heavy beans @Lazy instead of going global.

Shutting down gracefully

SpringApplication registers a JVM shutdown hook that closes the context on SIGTERM. Enable graceful web shutdown so in-flight requests finish:

server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s

The shutdown sequence: hook fires → web server stops accepting new connections → in-flight requests drain (up to the timeout) → SmartLifecycle.stop() → @PreDestroy/DisposableBean → context closed → JVM exits. In Kubernetes, keep Spring's timeout below terminationGracePeriodSeconds so rolling deploys never sever live requests. Because SIGKILL can't be trapped, treat @PreDestroy as best-effort cleanup, never as a correctness guarantee.

For batch and CLI apps, signal success or failure with an exit code:

@Component
class BatchJob implements ApplicationRunner, ExitCodeGenerator {
    private int exitCode = 0;
    public void run(ApplicationArguments args) { try { doWork(); } catch (Exception e) { exitCode = 1; } }
    public int getExitCode() { return exitCode; }
}

Recap

A Spring Boot app builds its Environment, creates and refresh()es the ApplicationContext (bean-factory post-processors edit definitions, bean post-processors wrap beans, singletons get instantiated), starts the embedded server, runs your ApplicationRunner/CommandLineRunner beans, and fires ApplicationReadyEvent. Hook bean setup with @PostConstruct, app-wide startup with a runner, post-context startup with SmartLifecycle, and "we're live" logic with ApplicationReadyEvent. Startup fails fast on bad config by design, and shutdown drains in-flight requests when you enable server.shutdown=graceful. Know this sequence and you always know which hook to reach for.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel