Spring Boot custom banners and startup hooks

Spring Boot applications usually begin with a familiar console banner, then continue silently while Spring creates beans, connects to databases and opens server ports. Those early messages are useful in development, but they can also conceal delays, duplicate tasks or failed assumptions in production.

Building a Spring Boot Application with Custom Banner and Startup Hooks gives teams a clearer path from startup to readiness. The banner communicates service identity, while startup hooks handle repeatable work that must finish before the application is declared ready.

Set up a local-first Spring Boot project

Start with Java 17 or 21 and a conventional Maven or Gradle project. Add the web and actuator starters, then keep environment-specific values in application.yml rather than embedding them in Java. A local profile can use PostgreSQL through Testcontainers, while ordinary unit tests avoid the full application context.

For an Australian product, keep money in AUD and model the standard 10% GST as configurable business data. Exemptions and tax treatment can differ, so startup code should not hard-code them. A small configuration record also keeps the Australian test and production environments clearly named.

Create a custom console banner

Place banner.txt in src/main/resources and replace the default artwork. Spring Boot resolves the file before logging the application banner, making it a convenient place for the service name, active profile and a short operational message.

  Australian Payments Hub
  ------------------------------
  Profile: ${spring.profiles.active:default}
  Pricing and payment services

The location can also be set explicitly in application.yml:

spring:
  application:
    name: payments-hub
  banner:
    location: classpath:banner.txt
  main:
    banner-mode: console

Use a separate banner-dev.txt for development and retain a restrained production banner, or set spring.main.banner-mode=off when infrastructure should expose no application artwork. Never include credentials, internal hostnames or customer information in banner content.

Run startup work with ApplicationRunner

ApplicationRunner is a Spring Boot interface for work that should run after the context is refreshed but before the application is declared ready. It receives parsed ApplicationArguments, which makes it useful for reading profile or feature switches.

@Component
@Order(10)
public class WarmPricingCache implements ApplicationRunner {

    private final PricingGateway gateway;

    public WarmPricingCache(PricingGateway gateway) {
        this.gateway = gateway;
    }

    @Override
    public void run(ApplicationArguments args) {
        gateway.refreshActiveProducts();
    }
}

The example warms a pricing cache through PricingGateway. Runners are synchronous: an uncaught exception fails startup, while slow calls delay the ready state. Give remote calls timeouts and make retries explicit. Use Flyway or Liquibase for schema migrations rather than hiding them in a runner.

Control ordering and readiness

When several runners exist, use @Order with distinct values; lower values run first. The interface also supports Ordered for ordering calculated at runtime. ApplicationRunner exposes parsed arguments, while CommandLineRunner receives raw command-line strings and is often chosen for simple scripts.

Reserve ApplicationReadyEvent for the final readiness marker or a short completion notification. It is published after the runners finish, so listeners can trust that their ordered startup work has completed. Keep these listeners fast, and avoid sending another slow network request there.

Choose the safest lifecycle hook

Bean constructors, afterPropertiesSet and @PostConstruct all run while individual beans are being created. They suit local initialisation and validation, but they are poor choices for remote calls because the rest of the context may not be ready. A constructor can also make unit tests slow if it performs I/O.

Use ApplicationRunner when startup depends on the assembled application context, and ApplicationReadyEvent when it must follow every runner. SmartLifecycle is another option for a managed component that needs paired start and stop behaviour. Whichever hook is selected, make its policy explicit and observable in logs and health information.

Test and document production behaviour

Startup hooks need tests because they execute under @SpringBootTest. Guard optional warming with @ConditionalOnProperty, then disable it in most unit tests and exercise it separately with a stub or Testcontainers. Assert that the expected beans were created and that the readiness event arrived in the expected order.

@ConditionalOnProperty(
    name = "app.cache-warmup.enabled",
    havingValue = "true",
    matchIfMissing = true
)

In production, every replica runs startup hooks, so duplicate cache refreshes or one-off jobs must be idempotent or protected by a distributed lock. Teams serving Sydney can select the AWS australiaeast region, while Melbourne teams can evaluate australiasoutheast; a multi-region rollout means separate hook executions in each location. For services covered by the Privacy Act 1988 and Australian Privacy Principles, startup logs can expose personal information, so customer identifiers and sensitive exception messages should be excluded.

Australia uses several time zones, and Sydney and Melbourne change their offsets with daylight saving, while Brisbane does not. Store timestamps in UTC, schedule jobs against an IANA time zone and record daylight-saving rules in the runbook. If a job must run at 02:30 during a clock change, use a library that handles the gap rather than adding an hour manually.

Document the banner, runner order, failure policy and configuration switches in the README and operations runbook. Onboarding can also connect engineers with general background material through an external learning portal, while deployment commands and ownership details remain in the repository. This arrangement makes local startup visible and production behaviour repeatable.