SpringApplication Bootstrap

1. Overview

Every EMS Spring Boot service — admin-service, registration-portal, future admin-portal — follows an identical bootstrap convention: main-class entry point that sets a few pre-context System properties, validates the active profile combination, starts the Spring context, and logs a multi-line startup banner describing what came up. The convention is inherited from JHipster and kept by choice because it has survived contact with production and gives operators a single consistent mental model across services.

This page documents what the banner contains, what profile validation rejects, and the small set of pre-context actions that every service main method performs.

2. Main Class Responsibilities

Each service has a single main class under its root package:

Service Main class

admin-service

za.co.idealogic.event.admin.AdminServiceApp

registration-portal

za.co.idealogic.registration.ui.EventregistrationuiApp

admin-portal (future)

za.co.idealogic.event.admin.portal.AdminPortalApp

The main class does three things, in order:

  1. Set any pre-context System properties (see below).

  2. Invoke SpringApplication.run(…​).

  3. Call logApplicationStartup(env) with the resulting ConfigurableEnvironment.

Additionally:

  1. @PostConstruct initApplication() validates the active profile combination after the context is ready.

2.1. Pre-context System properties

admin-service sets one before the context starts:

public static void main(String[] args) {
    System.setProperty("spring.devtools.restart.enabled", "false");
    SpringApplication app = new SpringApplication(AdminServiceApp.class);
    app.setDefaultProperties(defaultProfileProperties());
    ConfigurableApplicationContext ctx = app.run(args);
    Environment env = ctx.getEnvironment();
    logApplicationStartup(env);
}

Reason: DevTools' restart classloader is incompatible with Hazelcast — see Hazelcast Configuration § DevTools Compatibility.

registration-portal does not set this; its Hazelcast footprint is lighter. admin-portal should set it if it mirrors admin-service’s session replication + security dimension caches.

Other pre-context properties can appear here; rule of thumb: anything that needs to be true before @Bean methods execute.

3. Profile Validation

@PostConstruct on the main class:

@PostConstruct
public void initApplication() {
    Collection<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
    if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT)
            && activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) {
        log.error("You have misconfigured your application! It should not run with both the 'dev' and 'prod' profiles at the same time.");
    }
    if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT)
            && activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_CLOUD)) {
        log.error("You have misconfigured your application! It should not run with both the 'dev' and 'cloud' profiles at the same time.");
    }
}

Currently logs an error but does not fail-fast. A stronger variant would throw IllegalStateException and refuse to start — recommended if operators have ever shipped with a bad profile mix. Low-cost change.

4. Startup Banner

After the context is up, logApplicationStartup(env) logs a multi-line block:

----------------------------------------------------------
  Application 'event-admin-service' is running! Access URLs:
  Local:      http://localhost:12504
  External:   http://10.0.0.42:12504
  Profile(s): [prod, kubernetes, otlp]
----------------------------------------------------------
  Config Server:  -
  Database:       jdbc:mysql://idealogic-prod.mysql.svc.cluster.local:6446/wpca_prod
----------------------------------------------------------

Values pulled from the environment:

Field Source

Application name

spring.application.name

Protocol

server.ssl.key-store presence → https, else http

Server port

server.port

Context path

server.servlet.context-path

Local hostname

InetAddress.getLocalHost().getHostAddress() (best-effort; logs "localhost" on failure)

Profile(s)

env.getActiveProfiles(), or env.getDefaultProfiles() if none active

Config Server

spring.cloud.config.uri if present, else "-"

Database

spring.datasource.url with basic masking applied (strip username, password)

The banner is the operator’s first diagnostic: wrong port, wrong profile, wrong database URL — all visible in the first screenful of logs.

4.1. No ApplicationListener<ApplicationReadyEvent>

We considered moving the startup logic into an ApplicationReadyEvent listener. Pro: runs after embedded Tomcat is actually accepting requests. Con: the banner also sometimes precedes Hazelcast cluster-join logs, which is useful context. Current convention keeps the log in main() with a comment explaining the position.

If we move to Kubernetes readiness probes (/readyz) as the definitive "ready to serve" signal — we already do — the event listener variant becomes attractive. Deferred; not a blocker.

5. Default Profile Properties

SpringApplication.setDefaultProperties(defaultProfileProperties()) ensures that spring.profiles.default=dev — running without any profile argument uses dev rather than blowing up. This is a JHipster convention we keep.

Effect: java -jar event-admin-service.jar with no flags starts with dev profile. For prod, either --spring.profiles.active=prod,kubernetes on the command line or set SPRING_PROFILES_ACTIVE in the environment.

6. Profile Inventory

Profile What it toggles

dev

Default. Liquibase auto-apply with faker data, local H2/MySQL, Hazelcast standalone, DEBUG logging for za.co.idealogic, CORS relaxed

prod

Liquibase with contexts: prod only, MySQL, Hazelcast clustered discovery, INFO logging, CORS locked to configured origins

kubernetes

Spring Cloud Kubernetes config + discovery clients; Hazelcast K8s service-DNS discovery; plain-text logback (no ANSI)

api-docs

Enables springdoc.api-docs.enabled=true; CI spec extraction requires this (see OpenAPI Contract Flow)

otlp

OpenTelemetry spring-boot-starter + javaagent enabled, Logback OTLP appender (see OpenTelemetry Configuration)

tls

Configure server.ssl.* for HTTPS-origin mode; typically not used in-cluster (TLS terminates at ingress)

no-liquibase

Skip Liquibase on startup; used when the schema is managed out-of-band

oidc-dev (registration-portal only)

Local OIDC testing — enables a dev IdP client registration pointing at localhost Keycloak

Typical production profile set: prod,kubernetes,api-docs,otlp. Typical dev: just dev. The api-docs profile in prod is optional but keeps Swagger UI reachable for admin debugging; can be dropped if exposure is a concern.

7. Pre-context bootstrap.yml

admin-service has both bootstrap.yml and bootstrap-prod.yml. The bootstrap. family loads *before application.*, and is the right place for Spring Cloud Config client settings or any other property that other property sources depend on.

Typical contents:

spring:
  application:
    name: event-admin-service
  cloud:
    kubernetes:
      enabled: true
      config:
        enabled: true
      discovery:
        enabled: true

Portals can use a bootstrap.yml too; registration-portal’s is minimal. admin-portal should follow the same pattern.

8. Logback Configuration

Shared convention via logback-spring.xml:

  • Pattern includes a %crlf custom converter (CRLFLogConverter) — strips CRLF characters from log messages to prevent log-injection exploits

  • ANSI colours enabled under dev profile; plain-text under kubernetes (so collectors like Loki don’t swallow the escape sequences)

  • logback-appender for OTel trace correlation wired when otlp profile active

Reference: admin-service/src/main/resources/logback-spring.xml.

9. Graceful Shutdown

Spring Boot 3’s server.shutdown=graceful is enabled in all services. Shutdown behaviour:

  1. Container receives SIGTERM.

  2. Spring initiates graceful shutdown — new connections rejected, in-flight requests given up to spring.lifecycle.timeout-per-shutdown-phase (default 30s) to complete.

  3. Hazelcast member leaves the cluster; partition migration kicks in on remaining peers.

  4. JVM exits.

Kubernetes terminationGracePeriodSeconds should be > timeout-per-shutdown-phase + Hazelcast-leave-window. 60s is a safe default; tuned per service if Hazelcast partition counts grow.

10. Reference

File Role

admin-service/src/main/java/…​/AdminServiceApp.java

Main class; pre-context System property; profile validation; startup banner

registration-portal/src/main/java/…​/EventregistrationuiApp.java

Portal variant of the same pattern

admin-service/src/main/resources/config/bootstrap.yml

Spring Cloud Kubernetes bootstrap

admin-service/src/main/resources/logback-spring.xml

Logging config with CRLF converter, OTel appender

admin-service/src/main/resources/config/application.yml

Active profile defaults; common app properties

12. Change History

Date Change

2026-04-24

Initial draft. Captures current JHipster-derived bootstrap convention as explicit EMS standard.