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:
BeanFactoryPostProcessorruns in step 2 — it edits bean definitions before any bean exists.BeanPostProcessorruns in step 7 — it wraps each bean as it is created (this is how AOP proxies and@Autowiredinjection 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.