SpringApplication.run() bootstraps the entire application: it creates the
ApplicationContext, runs auto-configuration, instantiates and wires beans, starts the
embedded server, and finally hands control to your code.
public static void main(String[] args) {
SpringApplication.run(App.class, args);
// Under the hood, roughly:
// 1. Create a SpringApplication, infer the app type (servlet/reactive/none)
// 2. Run ApplicationContextInitializers and 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 (if web app)
// 7. Call ApplicationRunner / CommandLineRunner beans
// 8. Fire ApplicationReadyEvent — the app is now serving
}
The single most important step is refresh() on the context — that's where bean
definitions are processed, conditions evaluated, and singletons created. Everything else
is setup around it. Understanding this sequence explains where to hook custom startup
logic and why certain beans exist by the time your code runs.
Rule of thumb: run() = build the Environment, create and refresh() the context,
start the server, run the runners, fire ApplicationReadyEvent — in that order.
The ApplicationContext is Spring's IoC container — the registry that holds all bean
definitions, instantiates beans, injects dependencies, and manages their lifecycle. In
Spring Boot it's created and configured automatically by SpringApplication.
@SpringBootApplication
public class App {
public static void main(String[] args) {
// run() returns the fully-initialized context:
ApplicationContext ctx = SpringApplication.run(App.class, args);
// You can look up beans from it (rarely needed in app code):
MailService mail = ctx.getBean(MailService.class);
System.out.println(ctx.getBeanDefinitionCount() + " beans registered");
}
}
For a servlet web app, Spring Boot creates an AnnotationConfigServletWebServerApplicationContext;
for reactive, a reactive variant; for a plain app, a non-web context. The context extends
BeanFactory with extra features: event publishing, message resolution, resource loading
and environment access.
Rule of thumb: The ApplicationContext is the running application's bean container —
prefer dependency injection over pulling beans out of it with getBean().
A bean goes through instantiation → dependency injection → initialization → (use) → destruction. You hook the initialization and destruction phases with callbacks.
@Component
public class CacheWarmer {
@PostConstruct // runs after dependencies are injected
void init() {
// Good place to warm caches, open connections, validate config
System.out.println("Cache warmed");
}
@PreDestroy // runs on graceful shutdown, before bean is destroyed
void cleanup() {
// Release resources, flush buffers, close pools
System.out.println("Cache flushed");
}
}
The options, in order of preference: @PostConstruct/@PreDestroy (JSR-250, cleanest),
implementing InitializingBean/DisposableBean (couples you to Spring interfaces), or
@Bean(initMethod=..., destroyMethod=...) (good for third-party classes you can't
annotate). @PreDestroy only runs on an orderly shutdown, not a kill -9.
Rule of thumb: Use @PostConstruct/@PreDestroy for setup/teardown; for beans whose
class you don't own, use @Bean(initMethod, destroyMethod).
Both run code once, after the context is fully initialized but before
SpringApplication.run returns — ideal for one-off startup tasks. They differ only in
how they receive the program arguments.
@Component
public class StartupTasks implements CommandLineRunner {
@Override
public void run(String... args) { // raw String[] arguments
System.out.println("Starting with " + args.length + " args");
}
}
@Component
public class ParsedStartup implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) { // parsed: options vs non-options
boolean verbose = args.containsOption("verbose"); // --verbose
List<String> files = args.getNonOptionArgs(); // positional args
}
}
Use ApplicationRunner when you want parsed --option access; use CommandLineRunner for
the raw array. Order multiple runners with @Order or by implementing Ordered. These run
before ApplicationReadyEvent is fired.
Rule of thumb: Reach for a runner for "do this once at startup" tasks;
ApplicationRunner if you need parsed CLI options, CommandLineRunner for the raw args.
During startup Spring Boot publishes a sequence of ApplicationEvents you can listen to,
letting you hook precise moments without subclassing anything.
@Component
public class LifecycleListener {
@EventListener
void onReady(ApplicationReadyEvent event) {
// Fired LAST — context refreshed, runners done, server accepting traffic.
// Best place for "the app is live" actions (e.g. register with discovery).
}
@EventListener
void onFailed(ApplicationFailedEvent event) {
// Startup blew up — log diagnostics, alert, clean up partial state.
}
}
The ordered sequence: ApplicationStartingEvent → ApplicationEnvironmentPreparedEvent
→ ApplicationContextInitializedEvent → ApplicationPreparedEvent → (context refresh)
→ ApplicationStartedEvent → AvailabilityChangeEvent (READY) →
ApplicationReadyEvent, with ApplicationFailedEvent on error. The earliest events fire
before the context exists, so listen to them via spring.factories, not @EventListener.
Rule of thumb: Use ApplicationReadyEvent for "we're live" logic and
ApplicationFailedEvent for startup failure handling; @EventListener works from
ApplicationStartedEvent onward.
refresh() is the heart of container startup. It turns bean definitions into a fully
wired set of singleton beans, in a fixed sequence defined by AbstractApplicationContext.
// Conceptually, refresh() does (simplified):
// 1. prepareBeanFactory — set up the BeanFactory, register environment beans
// 2. invokeBeanFactoryPostProcessors
// → ConfigurationClassPostProcessor parses @Configuration, runs @Conditional,
// registers all bean definitions (this is where auto-config materializes)
// 3. registerBeanPostProcessors — register BeanPostProcessors (e.g. AOP, @Autowired)
// 4. initMessageSource / initApplicationEventMulticaster
// 5. onRefresh — create the embedded web server (web apps)
// 6. registerListeners
// 7. finishBeanFactoryInitialization — instantiate all non-lazy singletons
// 8. finishRefresh — publish ContextRefreshedEvent, start Lifecycle beans
Key insight: BeanFactoryPostProcessors run before any bean is instantiated (they edit
bean definitions), whereas BeanPostProcessors wrap beans as they're created. AOP
proxies, @Autowired injection, and @ConfigurationProperties binding all happen via
BeanPostProcessors in step 7.
Rule of thumb: BeanFactoryPostProcessor = edit definitions before instantiation;
BeanPostProcessor = wrap/modify each bean during instantiation — refresh runs them in
that order.
Graceful shutdown lets in-flight requests finish before the server stops accepting new ones, instead of dropping connections. Spring Boot supports it out of the box; you enable it with two properties.
# Stop accepting new requests, let active ones complete:
server.shutdown=graceful
# How long to wait for active requests before forcing shutdown:
spring.lifecycle.timeout-per-shutdown-phase=30s
// On SIGTERM (e.g. 'docker stop', Kubernetes pod termination):
// 1. The web server stops accepting new connections
// 2. In-flight requests are given up to the timeout to finish
// 3. @PreDestroy / DisposableBean callbacks run, Lifecycle beans stop
// 4. The JVM exits
Graceful shutdown matters in orchestrated environments: Kubernetes sends SIGTERM, waits
for terminationGracePeriodSeconds, then SIGKILLs. Aligning Spring's timeout below that
grace period prevents dropped requests during rolling deployments.
Rule of thumb: Set server.shutdown=graceful and a timeout shorter than your
orchestrator's grace period so rolling deploys never sever in-flight requests.
SmartLifecycle lets a bean participate in the context's start/stop phases with
explicit ordering — useful for components that must start after the context is ready and
stop before it tears down, like background pollers or message listeners.
@Component
public class MessageConsumer implements SmartLifecycle {
private volatile boolean running = false;
@Override public void start() { running = true; /* begin consuming */ }
@Override public void stop() { running = false; /* drain & disconnect */ }
@Override public boolean isRunning() { return running; }
// Higher phase = started later, stopped earlier. Default is Integer.MAX_VALUE.
@Override public int getPhase() { return 100; }
// true = start automatically when the context starts:
@Override public boolean isAutoStartup() { return true; }
}
Unlike @PostConstruct (which runs during bean creation), SmartLifecycle.start() runs at
the end of refresh, once all beans exist — so it's safe to start things that depend on
the whole context being ready. Phases give you deterministic start/stop ordering across
several lifecycle beans.
Rule of thumb: Use SmartLifecycle for components that must start after the full
context is up and stop in a controlled order — e.g. Kafka consumers, schedulers, sockets.
By default Spring instantiates all singleton beans eagerly during refresh(). Lazy
initialization defers a bean's creation until it's first needed, trading slower first-use
for faster startup.
@Component
@Lazy // this bean is created only when first injected/used
public class ExpensiveReportGenerator { /* heavy setup */ }
# Make EVERY bean lazy globally (use with care):
spring.main.lazy-initialization=true
Global lazy init speeds startup and is handy for dev or short-lived CLI tasks, but it has
real downsides: configuration errors and failed @PostConstruct logic surface on first
request instead of at startup, and you lose fail-fast behavior. You can opt specific beans
back to eager with @Lazy(false).
Rule of thumb: Default to eager init so problems fail fast at startup; use @Lazy
selectively for genuinely heavy, rarely-used beans rather than enabling it globally.
Spring orders bean creation automatically based on dependencies — a bean is created
after the beans it's injected with. When there's no direct dependency but order still
matters, use @DependsOn.
// FlywayMigrator has no field reference to the DataSource bean, but it must run after it:
@Component
@DependsOn("dataSource") // force dataSource to initialize first
public class FlywayMigrator { }
// Normal case — order is implied by injection, no annotation needed:
@Service
public class OrderService {
// Spring creates PaymentClient before OrderService automatically:
public OrderService(PaymentClient client) { }
}
@DependsOn is for initialization ordering between otherwise-unrelated beans (and it also
controls destruction order in reverse). For ordering collections of beans injected as a
List, use @Order instead. Avoid @DependsOn when a normal constructor dependency would
express the relationship more clearly.
Rule of thumb: Let constructor injection imply order; reach for @DependsOn only when
a required ordering exists without a direct dependency (migrations, infra bootstrapping).
Instead of the static SpringApplication.run(...), build a SpringApplication (or use
SpringApplicationBuilder) to tweak banners, profiles, listeners and the application type
before starting.
public static void main(String[] args) {
SpringApplication app = new SpringApplication(App.class);
app.setBannerMode(Banner.Mode.OFF); // no startup banner
app.setWebApplicationType(WebApplicationType.NONE); // run as a non-web app
app.setAdditionalProfiles("metrics"); // force-activate a profile
app.addListeners(new MyStartupListener()); // hook early events
app.setDefaultProperties(Map.of("server.port", "8081"));
app.run(args);
}
// Fluent alternative for multi-context apps:
// new SpringApplicationBuilder(App.class).profiles("prod").run(args);
This is how you adjust behavior that must be set before the context exists — like
registering listeners for the very early ApplicationStartingEvent, or forcing a non-web
app type for a batch job. Most apps never need this, but it's the right hook when you do.
Rule of thumb: Use a configured SpringApplication/SpringApplicationBuilder when you
must influence startup before the context is created (banner, app type, early listeners).
Both run startup logic, but at different points and scopes. @PostConstruct runs during a
single bean's initialization, before the rest of the context may be ready. A runner runs
once for the whole app, after the entire context is initialized.
@Component
public class Sample {
@PostConstruct
void perBeanInit() {
// Runs while THIS bean is being created — other beans may not exist yet.
// Use for: initializing this bean's own state, validating its config.
}
}
@Component
public class AppStartup implements CommandLineRunner {
public AppStartup(OrderService orders, MailService mail) { /* all beans ready */ }
@Override public void run(String... args) {
// Runs after the WHOLE context is up — safe to touch many collaborators.
// Use for: seeding data, kicking off jobs, cross-bean startup work.
}
}
Rule: if the work concerns one bean's own initialization, use @PostConstruct; if it
coordinates several beans or should happen once when the app is otherwise ready, use a
runner. Heavy or slow work in @PostConstruct also delays that bean's creation mid-refresh.
Rule of thumb: @PostConstruct for a single bean's own setup; a runner for app-wide,
cross-bean startup work that needs the full context in place.
For batch/CLI style apps you often want the process exit code to reflect success or
failure. Spring Boot resolves the exit code from ExitCodeGenerator beans (or
ExitCodeExceptionMapper for exceptions) and SpringApplication.exit(...).
@Component
public class BatchJob implements ApplicationRunner, ExitCodeGenerator {
private int exitCode = 0;
@Override public void run(ApplicationArguments args) {
try { doWork(); }
catch (Exception e) { exitCode = 1; } // signal failure to the shell
}
@Override public int getExitCode() { return exitCode; }
}
// For a non-web "run once and exit" app, close the context and exit explicitly:
// int code = SpringApplication.exit(context, () -> exitCode);
// System.exit(code);
Throwing an exception annotated with @ResponseStatus-like @ExitCodeExceptionsemantics,
or implementing ExitCodeExceptionMapper, lets specific exceptions map to specific codes.
This is essential for cron jobs and CI pipelines that branch on the exit status.
Rule of thumb: Implement ExitCodeGenerator (or SpringApplication.exit) so batch/CLI
apps return a meaningful non-zero exit code that schedulers and CI can act on.
SpringApplication automatically registers a JVM shutdown hook that closes the
ApplicationContext when the process receives SIGTERM (or finishes normally). Closing the
context runs destruction callbacks in reverse creation order.
@Component
public class ConnectionPoolHolder {
@PreDestroy
void close() {
// Runs when the context closes via the shutdown hook (orderly shutdown).
// Does NOT run on 'kill -9' / SIGKILL — there's no chance to clean up.
System.out.println("Closing connection pool");
}
}
// The hook is on by default; disable it only if you manage shutdown yourself:
// app.setRegisterShutdownHook(false);
The sequence on SIGTERM: shutdown hook fires → (graceful web shutdown if enabled) →
Lifecycle/SmartLifecycle.stop() → @PreDestroy/DisposableBean → context closed → JVM
exits. Because SIGKILL can't be trapped, never rely on @PreDestroy for correctness
(e.g. don't depend on it to commit data) — only for best-effort cleanup.
Rule of thumb: Trust the built-in shutdown hook for orderly cleanup via @PreDestroy,
but design for the case where it never runs (SIGKILL, power loss) — cleanup is best-effort.
If any non-lazy singleton throws during creation (constructor, @PostConstruct, or
injection), refresh() fails, the context is closed, an ApplicationFailedEvent is fired,
and the JVM exits non-zero — Spring Boot fails fast rather than running half-initialized.
@Component
public class ConfigChecker {
public ConfigChecker(@Value("${app.required-key}") String key) {
// If app.required-key is missing, the placeholder can't resolve →
// bean creation fails → startup aborts with a clear error, before serving traffic.
}
@PostConstruct
void validate() {
if (someInvariantBroken()) {
throw new IllegalStateException("Refusing to start: invalid configuration");
}
}
}
A registered FailureAnalyzer may translate the exception into a friendly "Description /
Action" block. This fail-fast behavior is a feature: it's far safer for a misconfigured app
to refuse to start than to start and serve broken responses. Throwing in @PostConstruct
is a legitimate way to enforce startup invariants.
Rule of thumb: Let startup fail fast on bad config — throw early in constructors or
@PostConstruct so a broken app never reaches the point of serving traffic.
More Core interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.