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. Profiles Describe Runtime Posture, Not Environment

dev and prod select a runtime posture, not a deployment environment. This distinction governs where a setting may legitimately live, and getting it wrong produces configuration that appears correct and does nothing.

Profile Posture it selects

dev

A permissive runtime built for feedback: verbose logging, relaxed CORS, stubs and mocks, faker data, auto-applied migrations, standalone caching.

prod

An optimised runtime built for service: restrained logging, locked-down CORS, real integrations, no fixtures, clustered caching, migrations gated by context.

prod therefore runs in every environment that needs an optimised runtime — which includes stage as well as production. Stage exists to exercise the production posture before production does; running it under dev would defeat that.

7.1. Consequence: a profile cannot identify an environment

Because stage and production share the prod profile, application-prod.yml applies to both. It cannot express "in production only".

This is the rule that follows:

Setting varies by… Where it belongs

Runtime posture (verbose vs optimised, stubbed vs real)

application-{profile}.yml in the service — this is exactly what profiles are for.

Environment (hostnames, credentials, endpoints, per-environment feature posture)

The deployment manifest, through the chart. Never a profile-specific file.

A setting that must differ between stage and production has only one home: the manifest. There is no profile that separates them, and adding one would mean stage no longer rehearses the production posture.

7.2. Charts must therefore pass configuration through

Because the manifest is the only per-environment discriminator, every deployable EMS service’s chart carries two generic passthroughs, so a new per-environment setting never requires a chart change:

Key Behaviour

config.extraProperties

Free-form YAML merged into the rendered application.yaml. Suited to nested configuration. Must not repeat a key the chart already renders — the result would be a duplicate YAML key.

extraEnv

Additional container environment variables, appended after the chart’s own. Environment variables outrank every file-based property source, so this is the mechanism when a value must override one baked into application.yml or application-{profile}.yml. Relaxed binding applies: ems.signal-sweep.enabled is EMS_SIGNALSWEEP_ENABLED.

Without a passthrough, a key the chart does not template renders as nothing at all — no error, no warning — while the manifest still reads as though the setting were applied. Prefer extraEnv whenever the value must beat a packaged default.

7.3. A default is a decision

A feature whose shipped default is enabled: true is enabled in every environment that does not override it, including production. Manifest silence then enables rather than disables. For anything with a production cost — a background scheduler, a poller, a reactor, anything writing to a shared schema — default it to disabled and let each environment opt in explicitly, so that switching it on is always a positive, visible act.

8. 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.

9. 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.

10. 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.

11. 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

13. Change History

Date Change

2026-04-24

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

2026-08-31

Added Not Environment: profiles select a runtime posture rather than an environment, stage runs prod, per-environment settings belong in the manifest, and the chart passthroughs that make that possible.