SpringApplication: Customizing Startup, Banner, Event Listeners

Master SpringApplication lifecycle customization including startup sequence, custom banners, and application event listeners for boot-time control.

published: reading time: 27 min read author: GeekWorkBench
Quick Summary

Master SpringApplication lifecycle customization including startup sequence, custom banners, and application event listeners for boot-time control. The guide uses practical examples to explain custom banner, text banner and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.

SpringApplication: Customizing Startup, Banner, Event Listeners

SpringApplication is the entry point that bootstraps and launches a Spring application. When you call SpringApplication.run(), a carefully orchestrated sequence springs into action: classpath scanning, environment preparation, context creation, bean loading, and finally the moment your app signals it’s ready to handle traffic. Knowing what happens at each step, and having the hooks to customize those steps, is what separates developers who just use Spring Boot from those who actually control it.

This post covers every major customization point: banners (text and image), runner interfaces (ApplicationRunner vs CommandLineRunner), the complete event hierarchy, programmatic configuration via SpringApplicationBuilder, environment post-processing, and the failure scenarios you need to anticipate in production. If you are new to Spring Boot, start with the Spring Boot Roadmap for a structured learning path.

Custom Banner

The startup banner is Spring Boot’s greeting card. Out of the box it prints the Spring Boot version and a ASCII flower graphic. You can replace this with your own branding, display build information, or suppress it entirely for quieter console output.

Text Banner

Create a banner.txt file in src/main/resources/ (classpath root). Spring Boot resolves this automatically at classpath:banner.txt.

 _____  _          __      __         _ _
/ ____|| |        /\ \    / /        | | |
\ `---.| |__     /  \ \  / /_ _ _   _| | |_
 <code>--.\| '_ \   / /\ \ \/ / _</code> | | | | | __|
/\__/ / | | | / ____ \  \ (_| | |_| | | |_
\____/|_| |_|/_/    \_\ \__,_|\__,_|_|\__|
${application.version} | Spring Boot ${spring-boot.version}

The banner supports placeholders and ANSI color codes:

Placeholder Description
${application.version} Version from MANIFEST.MF or Implementation-Version
${spring-boot.version} The running Spring Boot version
${application.title} Application title from MANIFEST.MF
${Ansi.RED} ANSI color codes for terminal styling

Image Banner

For pixel-art branding, place a .gif, .png, or .jpg image at classpath:banner.gif (or another supported extension). Spring Boot converts it to ASCII using a built-in converter. Control the image location explicitly:

spring.banner.image.location=classpath:banner.png
spring.banner.image.width=120
spring.banner.image.margin=2
spring.banner.image.invert=false

Programmatic Banner Control

Disable the banner entirely when running tests or in certain environments:

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(MyApplication.class);
        app.setBannerMode(Banner.Mode.OFF);
        app.run(args);
    }
}

Or using the builder pattern:

new SpringApplicationBuilder(MyApplication.class)
    .bannerMode(Banner.Mode.OFF)
    .run(args);

With @SpringBootApplication on the main class, you can also use properties:

spring.main.banner-mode=off

The Banner.Mode enum has three values: CONSOLE (print to System.out), LOG (log at INFO level), and OFF (suppress entirely). The default is CONSOLE.

When your banner lives outside the default classpath location:

spring.banner.location=file:./custom-banner.txt

Spring Boot registers the resolved banner as a singleton bean named springBootBanner, so you can also replace it entirely by defining your own Banner bean.

Application Runner vs CommandLineRunner

If you need to execute code after Spring Boot finishes starting but before it starts accepting traffic, two interfaces are worth knowing: CommandLineRunner and ApplicationRunner. They do the same job, but they receive arguments differently.

CommandLineRunner

Gets raw String[] arguments directly from main:

@Component
@Order(42)
public class MyCommandLineRunner implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        System.out.println("Raw args: " + Arrays.toString(args));
    }
}

ApplicationRunner

Gets an ApplicationArguments wrapper instead, which gives you convenient methods for inspecting named options:

@Component
@Order(1)
public class MyApplicationRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        boolean hasOption = args.containsOption("profile");
        List<String> nonOptionArgs = args.getNonOptionArgs();
        System.out.println("Profile flag present: " + hasOption);
    }
}

Execution Order

Both runner types run after the ApplicationReadyEvent is published. When you have multiple runners, they sort by Ordered value ascending (lower numbers run first). @Order(1) runs before @Order(42). Ties are broken by bean name alphabetically.

@Component
public class AlphaRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) { System.out.println("Alpha"); }
}

@Component
@Order(1)
public class FirstRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) { System.out.println("First"); }
}

Output: First, then Alpha.

When to Choose Which

Aspect CommandLineRunner ApplicationRunner
Argument format Raw String[] Parsed ApplicationArguments
Option parsing Manual Built-in containsOption(), getOptionValues()
Named arguments String comparison args.getOptionValues(“name”)
Use case Pass-through to legacy code Structured argument handling

For most Spring Boot applications, ApplicationRunner is the better choice because it handles argument parsing that you’d otherwise have to write yourself.

Application Events and Listeners

Spring Boot publishes a sequence of events from the moment SpringApplication.run() is called until the application is fully running or has failed. You can hook into each phase if you know the order.

The Event Sequence

graph TD
    A[ApplicationStartingEvent] --> B[ApplicationEnvironmentPreparedEvent]
    B --> C[ApplicationContextInitializedEvent]
    C --> D[ApplicationPreparedEvent]
    D --> E[ContextRefreshedEvent]
    E --> F[ApplicationStartedEvent]
    F --> G[LivenessState.CORRECT]
    G --> H[ApplicationReadyEvent]
    H --> I[ReadinessState.ACCEPTING_TRAFFIC]
    A -.-> J[ApplicationFailedEvent]
    D -.-> J

Event Details

ApplicationStartingEvent fires earliest, before the Environment or ApplicationContext exists. Listeners must be registered programmatically via SpringApplication.addListeners() or through spring.factories (or org.springframework.boot.BootstrapConfiguration in Spring Boot 3.x).

ApplicationEnvironmentPreparedEvent fires once the ConfigurableEnvironment is ready. At this point you can modify property sources, add profiles, or inspect configuration.

ApplicationContextInitializedEvent fires after the ApplicationContext is constructed and ApplicationContextInitializers have been called, but before bean definitions are loaded.

ApplicationPreparedEvent fires after the context is fully prepared with bean definitions loaded, but before refresh.

ApplicationStartedEvent fires after the context has been refreshed. At this point the app has started but CommandLineRunner and ApplicationRunner beans have not yet executed.

ApplicationReadyEvent fires after runners complete. The application is fully operational.

ApplicationFailedEvent fires whenever startup fails, with access to the thrown exception and the (partial or null) context.

Registering Listeners

As a Bean (context-available events)

For events fired after the ApplicationContext exists, annotate a bean with @Component and implement ApplicationListener<EventType>:

@Component
public class MyStartupListener implements ApplicationListener<ApplicationReadyEvent> {

    private static final Logger log = LoggerFactory.getLogger(MyStartupListener.class);

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        log.info("Application ready. Uptime: {} ms",
            System.currentTimeMillis() - event.getTimestamp());
    }
}

Programmatic Registration (early events)

For ApplicationStartingEvent and ApplicationEnvironmentPreparedEvent, the context does not yet exist, so @Bean listeners won’t be found. Register listeners before run() completes:

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(MyApplication.class);
    app.addListeners(new MyEarlyListener());
    app.run(args);
}

Or via SpringApplicationBuilder:

new SpringApplicationBuilder(MyApplication.class)
    .listeners(new MyEarlyListener())
    .run(args);

Via spring.factories (Spring Boot 2.x) / META-INF/spring/org.springframework.boot.BootstrapConfiguration (Spring Boot 3.x)

For auto-configuration scenarios, register listeners in the SpringFactoriesLoader format.

SpringApplicationEvent Hierarchy

All Spring Boot application events extend SpringApplicationEvent, which itself extends Spring Framework’s ApplicationEvent:

ApplicationEvent (Spring Framework)
  └── SpringApplicationEvent
        ├── ApplicationStartingEvent
        ├── ApplicationEnvironmentPreparedEvent
        ├── ApplicationContextInitializedEvent
        ├── ApplicationPreparedEvent
        ├── ApplicationStartedEvent
        ├── ApplicationReadyEvent
        └── ApplicationFailedEvent

Standard Spring Framework events like ContextRefreshedEvent (from AbstractApplicationContext) also fire during startup, but they belong to the Spring Framework event model, not the Spring Boot-specific SpringApplicationEvent hierarchy. See Spring Framework Core for more on the standard Spring event model.

Implementation Snippets

Custom SpringApplicationBuilder Configuration

The fluent builder API lets you configure the application before run() is called:

new SpringApplicationBuilder(MyApplication.class)
    .properties("server.port=8080")
    .profiles("production")
    .bannerMode(Banner.Mode.OFF)
    .listeners(new MetricsPublisher())
    .run(args);

This comes in handy in test setups or when you need environment-specific bootstrapping logic in main() before auto-configuration takes over.

Custom Banner Class

Replace the default banner entirely with your own implementation:

@Component
public class CustomBanner implements Banner {

    @Override
    public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
        out.println("========================================");
        out.println("  CUSTOM APPLICATION v" + environment.getProperty("app.version"));
        out.println("========================================");
    }
}

Wire it in by setting it on the SpringApplication instance:

SpringApplication app = new SpringApplication(MyApplication.class);
app.setBanner(new CustomBanner());
app.run(args);

ApplicationListener Implementation

For cleaner separation, use SmartApplicationListener which gives you event-type filtering:

@Component
public class EnvironmentDumper
    implements SmartApplicationListener {

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        ConfigurableEnvironment env = event.getEnvironment();
        env.getPropertySources().forEach(ps ->
            System.out.println("PropertySource: " + ps.getName()));
    }

    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 20;
    }
}

SmartApplicationListener gives you supportsEventType() and supportsSourceType() filters, which is more flexible than the raw ApplicationListener interface.

EnvironmentPostProcessor

EnvironmentPostProcessor runs before the ApplicationContext is created. It mutates the ConfigurableEnvironment, adding custom property sources. This is the same mechanism behind built-in support for application.yml and application.properties.

public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor {

    private final YamlPropertySourceLoader loader = new YamlPropertySourceLoader();

    @Override
    public void postProcessEnvironment(
            ConfigurableEnvironment environment,
            SpringApplication application) {

        Resource path = new ClassPathResource("config.yml");
        if (path.exists()) {
            try {
                List<PropertySource<?>> sources = loader.load("custom-yaml", path);
                environment.getPropertySources().addLast(sources.get(0));
            } catch (IOException ex) {
                throw new IllegalStateException("Failed to load config.yml", ex);
            }
        }
    }
}

Register it in META-INF/spring/org.springframework.boot.BootstrapConfiguration (Spring Boot 3.x):

org.springframework.boot.env.EnvironmentPostProcessor=\
  com.example.myapp.MyEnvironmentPostProcessor

Property sources added via addLast() have the lowest precedence, so existing properties win. Use addFirst() when you need your values to override defaults. This trips people up constantly.

Failure Scenarios

Listeners Throwing Exceptions

If an ApplicationListener throws an uncaught exception, the startup sequence stops and a MultiStackTraceException wraps the original error. The ApplicationFailedEvent gets published if the context existed at that point.

@Component
public class BadListener implements ApplicationListener<ApplicationReadyEvent> {
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        throw new RuntimeException("Listener failed");  // blocks startup
    }
}

Always wrap listener logic in try-catch if the code can fail, or use @Async for non-critical side effects.

Blocking Startup with Long-Running Listeners

Network calls, infinite loops, or blocking I/O inside listeners block the entire startup sequence. The app never reaches ApplicationReadyEvent, and Kubernetes health probes may report the app as dead even though the JVM is still running.

@Component
public class SlowListener implements ApplicationListener<ApplicationReadyEvent> {
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // DANGEROUS: blocks startup
        Thread.sleep(30_000);
    }
}

Use @Async with a dedicated thread pool for any listener work that is not strictly part of the startup critical path.

Circular Dependencies During Refresh

If your listener beans have circular dependencies with other @Component beans, the ContextRefreshedEvent phase fails with a BeanCurrentlyInCreationException. This is a Spring Framework issue, but it surfaces during the refresh phase of SpringApplication.run(). Break the cycle with @Lazy on one of the dependencies, or refactor.

Bean Initialization Order

@PostConstruct on listener beans runs during bean initialization, which happens before ContextRefreshedEvent. So a class that both has @PostConstruct and implements ApplicationListener<ContextRefreshedEvent> will see its @PostConstruct callback fire, and then later the event callback fire for the same logical event. Pick one mechanism and stick with it.

Trade-off Table

ApplicationRunner vs CommandLineRunner

Criteria ApplicationRunner CommandLineRunner
Argument representation ApplicationArguments Raw String[]
Option parsing effort Zero (built-in API) Manual string parsing
Named argument support Yes (–name=value) No
Multi-value option support Yes (getOptionValues()) No
Non-option argument handling Yes (getNonOptionArgs()) Yes (all args)
Framework preference Modern, recommended Legacy compatibility

@PostConstruct vs ApplicationListener

Criteria @PostConstruct ApplicationListener
Phase Bean initialization Event-based (context refresh)
Context availability Partial (beans created, context not refreshed) Full (context refreshed)
Exception handling Fails bean creation Can block startup (if not caught)
Ordering control @Order, depends-on @Order, listener priority
Multiple events Single callback Can listen to multiple event types
Testability Requires context Can test event firing in isolation
Use case Simple initialization Cross-cutting lifecycle reactions

When to Use / When NOT to Use

Reach for ApplicationRunner or CommandLineRunner When

  • You need to run initialization code exactly once after the app context is fully refreshed
  • You need access to Spring beans for your startup logic
  • You want to control execution order across multiple initializers
  • The initialization is critical path (must succeed for the app to be considered healthy)

Don’t Reach for ApplicationRunner / CommandLineRunner When

  • You only need to configure a single bean (use @PostConstruct or InitializingBean)
  • Your logic is purely infrastructural (put it in an @Configuration class instead)
  • You need to run code before the context is created (use EnvironmentPostProcessor or SpringApplicationRunListener)
  • You need to react to application lifecycle changes beyond “just after startup” (use ApplicationListener)

Reach for ApplicationListener When

  • You need to react to a specific lifecycle event (not just “after startup”)
  • You want to hook into early bootstrap phases (programmatic registration required)
  • You need the same listener to handle multiple different event types
  • You are building a framework or library that participates in Spring Boot’s lifecycle

Don’t Reach for ApplicationListener When

  • You only need to initialize one bean’s state (use @PostConstruct)
  • You need to run code before beans are created (use EnvironmentPostProcessor)
  • Your logic is synchronous and can block (consider @Async wrapping)

Observability Checklist

Tracking startup performance helps catch regressions before they hit production. Combine these techniques with Spring Boot Actuator for a full observability picture.

  • Record System.nanoTime() when ApplicationStartingEvent fires, then compare against ApplicationReadyEvent.timeTaken for total startup duration
  • Inside listeners, capture timestamps at environmentPrepared and contextLoaded phases to build a breakdown
  • Micrometer’s Timer in runners exposes startup duration as a metric automatically:
@Component
public class StartupTimerRunner implements ApplicationRunner {

    private final MeterRegistry registry;

    public StartupTimerRunner(MeterRegistry registry) {
        this.registry = registry;
    }

    @Override
    public void run(ApplicationArguments args) {
        registry.timer("startup.total").record(Duration.ofMillis(
            System.currentTimeMillis() - appStartTime));
    }
}
  • Emit a custom ApplicationEvent at the end of your runner chain so downstream systems can react
  • Add structured log fields (MDC) with bootstrap phase and request ID to correlate logs during startup. See Spring Boot Logging Configuration for MDC setup details.

Security and Compliance Notes

Sensitive Data in Banners

Never put secrets, API keys, or environment-specific information in banner.txt. The banner prints to System.out before logging is fully configured, and many deployment platforms (Cloud Foundry, Heroku) capture console output in dashboards and logs that may be accessible to unauthorized users.

# UNSAFE - do not do this
My App | API_KEY=${API_KEY} | DB_PASSWORD=${DB_PASSWORD}

Even in internal environments, banners that echo environment variables risk accidental exposure in log aggregators.

Environment Variables During Bootstrap

Between ApplicationStartingEvent and ApplicationEnvironmentPreparedEvent, environment variables are already visible to the process. If your listeners log System.getenv() outputs, those values may end up in application logs. Treat the bootstrap phase as security-sensitive: log as little as possible before ApplicationReadyEvent.

Set spring.main.banner-mode=off in production. A noisy banner adds no value and consumes bandwidth in your log pipeline.

TLS Configuration for Production

If you configure a custom WebServerFactory with SSL, ensure TLS protocols are restricted to modern versions:

@Configuration
public class SslConfig {
    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.setSsl(ssl -> ssl
            .setEnabledProtocols("TLSv1.3", "TLSv1.2")
            .setEnabledCipherSuites(Arrays.asList(
                "TLS_AES_256_GCM_SHA384",
                "TLS_AES_128_GCM_SHA256"
            ))
        );
        return factory;
    }
}

Disable fallback to older protocols (TLSv1.0, TLSv1.1) which have known vulnerabilities.

Access Controls for ApplicationListeners

ApplicationListener beans that interact with sensitive resources should declare explicit role requirements or method-level security:

@Component
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class AuditStartupListener implements ApplicationListener<ApplicationReadyEvent> {

    private final AuditService auditService;

    public AuditStartupListener(AuditService auditService) {
        this.auditService = auditService;
    }

    @Override
    @PreAuthorize("hasRole('ADMIN')")
    public void onApplicationEvent(ApplicationReadyEvent event) {
        auditService.recordStartup(event.getTimestamp());
    }
}

Mark infrastructure components with @Role(BeanDefinition.ROLE_INFRASTRUCTURE) to distinguish them from application beans in security reviews.

Audit Logging for Startup Events

For compliance requirements (SOC2, ISO 27001), log startup events with enough detail to reconstruct what happened:

@Component
public class ComplianceStartupListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

    private static final Logger log = LoggerFactory.getLogger(ComplianceStartupListener.class);

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        Environment env = event.getEnvironment();
        log.info("STARTUP_AUDIT: environment={}, profiles={}, java_version={}",
            env.getProperty("spring.application.name"),
            String.join(",", env.getActiveProfiles()),
            System.getProperty("java.version")
        );
    }
}

Capture: application name, active profiles, Java version, startup timestamp, and hostname. Do not log secrets or passwords. Store startup logs with appropriate retention for your compliance framework.

Set spring.main.banner-mode=off in production. A noisy banner adds no value and consumes bandwidth in your log pipeline.

Common Pitfalls

@PostConstruct Order: @PostConstruct runs during bean initialization, before ContextRefreshedEvent. A class that combines @PostConstruct with ApplicationListener<ContextRefreshedEvent> gets two callbacks for one logical event. This catches everyone at least once.

Listener Exceptions Blocking Startup: An uncaught exception in any ApplicationListener (except via @Async) halts the entire startup sequence. Wrap risky code:

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
    try {
        doRiskyThing();
    } catch (Exception ex) {
        logger.error("Startup task failed", ex);
    }
}

Multiple CommandLineRunners with Same @Order: When two runners share the same @Order value, Spring breaks the tie by bean name alphabetically. This is deterministic but surprising. Use unique order values when runners have implicit dependencies.

Listener Registration Timing: A @Bean-scanned listener cannot receive ApplicationStartingEvent or ApplicationEnvironmentPreparedEvent because those fire before the bean factory exists. For early events, use programmatic registration or a BootstrapConfiguration file.

Banner Caching: Spring Boot caches the resolved banner. If you swap banner.txt at runtime for multi-tenant deployments, remember to call app.setBanner() before each run() call or the change won’t take effect in the same JVM session.

EnvironmentPostProcessor Property Source Precedence: addLast() puts your property source at the bottom of the priority chain, so defaults win. If your custom properties are being ignored, check whether you should be using addFirst() instead.

Quick Recap Checklist

  • Spring Boot fires events in this order: Starting → EnvironmentPrepared → ContextInitialized → Prepared → Started → Ready
  • ApplicationStartingEvent and ApplicationEnvironmentPreparedEvent fire before the bean factory exists; @Bean listeners won’t catch them
  • ApplicationRunner beats CommandLineRunner for modern apps because ApplicationArguments handles parsing for you
  • Control runner order with @Order — lower numbers run first
  • EnvironmentPostProcessor mutates the environment before the context exists; registered in BootstrapConfiguration
  • Uncaught listener exceptions halt startup; wrap risky code in try-catch
  • @PostConstruct fires before ContextRefreshedEvent on the same bean — don’t mix the two without knowing the order
  • Set spring.main.banner-mode=off in production
  • Never echo secrets or env vars in banner.txt
  • SmartApplicationListener is more expressive than raw ApplicationListener when you need fine-grained event filtering

Interview Questions

1. What is the order of execution between ApplicationStartedEvent, ApplicationReadyEvent, and ContextRefreshedEvent during Spring Boot startup?

The correct order is: ContextRefreshedEvent fires first when the AbstractApplicationContext refreshes the internal bean factory. Then ApplicationStartedEvent fires after the context has been refreshed but before CommandLineRunner and ApplicationRunner beans execute. Finally, ApplicationReadyEvent fires after all runners have completed and the application is fully prepared to handle requests. Note that ContextRefreshedEvent is a Spring Framework event (not Spring Boot-specific), while the other two are Spring Boot SpringApplicationEvent subclasses.

2. How do you register an ApplicationListener for ApplicationStartingEvent when the ApplicationContext does not exist yet?

Since ApplicationStartingEvent fires before the ApplicationContext is created, @Bean-scanned listeners are not available. You must register the listener before calling run(). Three options: call springApplication.addListeners(new MyListener()) in main(); use the SpringApplicationBuilder fluent API with .listeners(new MyListener()); or register it in META-INF/spring/org.springframework.boot.BootstrapConfiguration for auto-configuration scenarios. The spring.factories approach from Spring Boot 2.x is deprecated in 3.x.

3. What is the difference between ApplicationRunner and CommandLineRunner, and when would you choose one over the other?

CommandLineRunner receives raw String[] arguments as they arrive at main(), leaving you to parse named options manually. ApplicationRunner gives you an ApplicationArguments wrapper with methods like containsOption(), getOptionValues(), and getNonOptionArgs(), eliminating that boilerplate. Choose ApplicationRunner when your app accepts named arguments like --profile=dev. Choose CommandLineRunner only when you need raw argument access or are working with legacy code that already processes String[].

4. What happens if an ApplicationListener throws an uncaught exception during startup, and how can you prevent it from crashing the application?

An uncaught exception propagating from any ApplicationListener (unless wrapped in @Async) terminates the startup sequence immediately. The ApplicationFailedEvent is published if the context existed at that point, but the application does not start. Wrap dangerous code in try-catch inside the listener method, log the error, and decide whether it is actually fatal. For side effects that do not need to block startup, add @Async so the listener runs on a separate thread pool. For critical initialization that must succeed, let the exception propagate so the app fails fast instead of running in a broken state.

5. How does EnvironmentPostProcessor differ from ApplicationListener, and when would you use each?

EnvironmentPostProcessor operates at the environment level before the ApplicationContext exists. It can add, remove, or reorder property sources in ConfigurableEnvironment, shaping how all subsequent configuration gets resolved. This is the same mechanism that powers built-in support for application.yml. ApplicationListener operates after the ApplicationContext exists and responds to lifecycle events. Use EnvironmentPostProcessor when you need to inject configuration that other components will read during initialization. Use ApplicationListener when you need to react to the application reaching a specific lifecycle state, such as logging startup metrics or triggering warm-up tasks.

6. What is the purpose of the Banner interface in Spring Boot, and how would you implement a custom Banner that displays dynamic build information?

The Banner interface in Spring Boot defines a contract for rendering the startup banner. Implementing it requires defining a printBanner() method that receives the Environment, Class source, and a PrintStream for output. A custom banner can read build-time properties from Environment such as app.version, spring-boot.version, or any custom property injected at build time via Maven or Gradle resource filtering. Register the custom banner by calling springApplication.setBanner(new MyBanner()) in main(), or by defining it as a bean so it replaces the default springBootBanner singleton. For fully dynamic content, the Environment gives you access to any property, including those loaded from external config files.

7. Explain the difference between SmartApplicationListener and ApplicationListener. When would you choose SmartApplicationListener?

ApplicationListener is the basic interface — you implement onApplicationEvent() and receive every event passed to your bean. SmartApplicationListener extends this with two additional methods: supportsEventType() lets you declare which event types your listener accepts, avoiding the need for instanceof checks inside onApplicationEvent(). supportsSourceType() filters by the source object that published the event. Choose SmartApplicationListener when you need fine-grained filtering without cluttering your event handler with type checks, or when you want to participate in Spring's ordered listener mechanism via getOrder(). It is particularly useful when writing infrastructure components that should only react to specific event types from specific sources.

8. What is the role of ApplicationEnvironmentPreparedEvent, and what can you do with the ConfigurableEnvironment at that point?

ApplicationEnvironmentPreparedEvent fires once the ConfigurableEnvironment is assembled but before the ApplicationContext is created. At this stage you can add, remove, or reorder property sources — including environment variables, system properties, command-line arguments, and files like application.yml. You can also activate Spring profiles via environment.addActiveProfile(). These changes influence how all subsequent beans and configuration are resolved. This is the last point in the bootstrap sequence where you can shape the environment without triggering bean creation. The event also gives you access to the SpringApplication instance, so you can modify application-level settings such as banner mode or bean factory classes before the context is constructed.

9. How does Spring Boot's failure handling work? What happens to the ApplicationContext when startup fails, and how can you recover?

When an uncaught exception propagates during SpringApplication.run(), Spring Boot publishes an ApplicationFailedEvent containing the exception and the partial or null context. If the context was already created, it gets closed to release resources. The JVM exits with a non-zero code. To handle failures gracefully, register an ApplicationListener for ApplicationFailedEvent that performs cleanup — closing connections, alerting monitoring systems, or logging diagnostic information. You can also use SpringApplicationBuilder to attach a failureAnalyzer or configure a FailureAnalyzer bean that transforms raw exceptions into user-friendly messages. Note that recovery after a failed startup requires a fresh run() call; Spring Boot does not automatically retry.

10. What is the relationship between ContextRefreshedEvent and ApplicationStartedEvent? Why does Spring Boot define its own event types?

ContextRefreshedEvent originates from Spring Framework's AbstractApplicationContext and fires whenever any Spring context is refreshed, including contexts outside Spring Boot. ApplicationStartedEvent is Spring Boot-specific, firing after the context refresh but before CommandLineRunner and ApplicationRunner execute. Spring Boot defines its own event hierarchy (extending SpringApplicationEvent) to model the specific phases of a Spring Boot application's lifecycle, which go beyond what a generic Spring context provides — such as banner printing, runner execution, and readiness state. This distinction matters for libraries and frameworks that listen to Spring events: a ContextRefreshedEvent listener picks up every context refresh, while a Spring Boot listener only reacts to Spring Boot-specific lifecycle transitions.

11. How does the SpringApplicationBuilder fluent API work, and what are some non-obvious configuration options it provides?

SpringApplicationBuilder exposes a fluent API that mirrors the SpringApplication configuration pipeline. It allows parent-child context hierarchies via .parent() and .child(), which is useful for modular applications where you want isolated bean namespaces. It also provides .application() to set the application class, .profiles() for profile activation, .properties() for inline property injection, and .bannerMode() to control banner output. A non-obvious option is .web(WebType) which forces the web environment type, bypassing Spring Boot's auto-detection logic when you need deterministic behavior. Another is .headless(boolean) which controls whether JVM graphics subsystems are available — useful in containerized environments where no display is present.

12. What happens during the ContextRefreshedEvent phase that makes it different from ApplicationReadyEvent from a bean lifecycle perspective?

ContextRefreshedEvent fires when AbstractApplicationContext.refresh() completes, meaning the bean factory has been fully loaded and all bean post-processing (including @PostConstruct and InitializingBean.afterPropertiesSet()) has run. However, CommandLineRunner and ApplicationRunner have not yet executed, and the application is not yet considered fully operational. ApplicationReadyEvent comes after runners complete, signaling that the application is ready to serve traffic. The critical distinction is timing: ContextRefreshedEvent means beans are initialized; ApplicationReadyEvent means the entire startup sequence including runners has finished. Listeners reacting to ContextRefreshedEvent must not assume that startup-side effects like runners have completed.

13. Why is it dangerous to perform blocking I/O or network calls inside an ApplicationListener for ApplicationReadyEvent without safeguards?

ApplicationReadyEvent fires on the main thread before the application begins accepting network connections. Any blocking operation on that thread — synchronous HTTP calls, Thread.sleep(), or file I/O — delays or prevents the application from entering a listening state. In containerized deployments, Kubernetes liveness probes may fire during this delay, detect that the app is unresponsive, and force a restart even though the JVM is alive. The safest approach for resource-intensive initialization triggered by ApplicationReadyEvent is to use @Async with a dedicated thread pool, or to offload the work to a CommandLineRunner/ApplicationRunner that also runs asynchronously. Alternatively, use Spring's TaskExecutor to submit the work without blocking the event dispatcher thread.

14. What is the difference between addFirst() and addLast() when registering PropertySources via EnvironmentPostProcessor, and why does it matter?

Spring Boot's ConfigurableEnvironment maintains a chain of PropertySource objects with a defined precedence order — the first source in the chain wins when resolving a property key. addFirst() inserts your property source at the top of the chain, giving it highest priority and making its values override everything else including environment variables and command-line arguments. addLast() appends to the bottom, meaning existing property sources take precedence and your values serve as defaults. This matters when implementing EnvironmentPostProcessor: if you want your custom configuration to be overridable by the application's application.yml, use addLast(). If you need mandatory override behavior, use addFirst(). The most common mistake is using addLast() when addFirst() was intended, resulting in custom properties being silently ignored.

15. How would you use SpringApplicationRunListener to instrument startup phases for observability, and what is the minimum implementation required?

SpringApplicationRunListener is the SPI that allows instrumentation of all major startup phases. The minimal implementation requires a constructor accepting SpringApplication and String[] args, plus one or more of the callback methods: starting(), environmentPrepared(), contextPrepared(), contextLoaded(), started(), running(), and failed(). For observability, record timestamps at each phase and emit them to your monitoring system (Micrometer, SLF4J MDC, OpenTelemetry). Register the listener in META-INF/spring/org.springframework.boot.BootstrapConfiguration so it is loaded before the application context exists. Spring Boot Actuator's /actuator/startup endpoint internally uses SpringApplicationRunListener to capture the startup steps, so implementing this interface also makes your custom phases visible in the Actuator startup endpoint.

16. What is the purpose of LivenessState and ReadinessState in Spring Boot, and how do they relate to ApplicationReadyEvent?

Introduced in Spring Boot 2.6, LivenessState.CORRECT and ReadinessState.ACCEPTING_TRAFFIC represent the internal health of the application and its readiness to receive traffic. LivenessState.CORRECT fires after ApplicationStartedEvent, signaling that the application context has been refreshed and the JVM is not in a failed state. ReadinessState.ACCEPTING_TRAFFIC fires after ApplicationReadyEvent, indicating that runners have completed and the web server is actively accepting connections. Kubernetes uses these signals for liveness and readiness probe decisions. An application can be lalive but not yet ready — for example, when a critical cache is still loading after the context refreshes. Understanding these states helps you avoid sending traffic to an app that has started but not yet finished its initialization logic.

17. How does Spring Boot resolve the web environment type (servlet vs reactive) when no explicit configuration is provided?

Spring Boot auto-detects the web environment by inspecting the classpath for the presence of specific classes. If jakarta.servlet.Servlet or javax.servlet.Servlet is on the classpath, it selects the servlet-based web environment. If Spring WebFlux's WebHandler is present, it selects reactive. If neither is found, it uses a non-web environment. You can override this behavior explicitly via spring.main.web-application-type in properties, or by calling .web(WebApplicationType.NONE) on SpringApplicationBuilder. This matters because the web environment type determines which ApplicationContext subclass gets created (AnnotationConfigServletWebServerApplicationContext vs AnnotationConfigReactiveWebServerApplicationContext vs generic AnnotationConfigApplicationContext), which in turn affects bean scanning, auto-configuration, and server initialization.

18. What are the implications of using @Order on CommandLineRunner and ApplicationRunner beans when they have dependencies on each other?

@Order controls execution sequence, but it does not manage bean instantiation order. If Runner A has a dependency on Runner B, Runner B must be instantiated first, which may conflict with the order you specified for execution. Spring does not re-order bean creation based on @Order — only the invocation order of the run() method is sorted. The safest approach for dependent initialization is to consolidate the logic into a single runner, or to use @DependsOn({"runnerB"}) on runner A to ensure B is fully initialized before A starts. Alternatively, use a SmartInitializingSingleton that Spring calls after all beans are created, which gives you a single hook where you can invoke multiple initialization steps in the correct dependency order.

Further Reading

Conclusion

SpringApplication is the orchestration layer that bootstraps your Spring Boot application, firing events in a predictable sequence that you can hook into at every stage. The key customization points—banners, runners, event listeners, and programmatic configuration via SpringApplicationBuilder—give you control over startup behavior without modifying your application code.

Remember that early events like ApplicationStartingEvent and ApplicationEnvironmentPreparedEvent fire before the bean factory exists, so programmatic registration is required. Use ApplicationRunner over CommandLineRunner for modern argument parsing, and always protect sensitive endpoints exposed during bootstrap. The startup sequence is not just infrastructure—it is the first opportunity to validate your application’s health and readiness.

Category

Related Posts

Spring Boot Build Tools: Maven & Gradle

Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.

#spring-boot #spring-boot-roadmap #learning-path

Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow

Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.

#spring-boot #spring-boot-roadmap #learning-path

JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests

Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.

#spring-boot #spring-boot-roadmap #learning-path