Skip to content

Application Lifecycle Interview Questions & Answers

16 questions Updated 2026-06-26 Share:

The Spring Boot application lifecycle — what SpringApplication.run does, context refresh, bean lifecycle callbacks, runners, application events, and graceful shutdown.

Read the in-depth guideThe Spring Boot Application Lifecycle, Start to Shutdown(opens in new tab)
16 of 16

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.

The banner is the ASCII art Spring logo printed at startup. It's cosmetic but commonly customized to show app name, version and active profiles in logs.

# src/main/resources/banner.txt — Spring Boot renders it and substitutes variables:
  ___  ___  ___  ___ ___  ___
 Orders Service
 Version: ${application.version}
 Profile: ${spring.profiles.active}
 Spring Boot ${spring-boot.version}
# Turn it off, or render to a file:
spring.main.banner-mode=off          # off | console | log
// Or set it programmatically:
// app.setBannerMode(Banner.Mode.OFF);

Placeholders like ${application.version} resolve from the build's manifest (populated by the Spring Boot Maven/Gradle plugin), so the banner can confirm exactly which version and profile are running — handy when scanning container logs.

Rule of thumb: Put a banner.txt with ${application.version} and ${spring.profiles.active} so every startup log self-identifies; set banner-mode=off where logs need to stay terse.

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 ways to practice

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

Join our WhatsApp Channel