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.
See also: Portal Pattern, Hazelcast Configuration.
2. Main Class Responsibilities
Each service has a single main class under its root package:
| Service | Main class |
|---|---|
admin-service |
|
registration-portal |
|
admin-portal (future) |
|
The main class does three things, in order:
-
Set any pre-context System properties (see below).
-
Invoke
SpringApplication.run(…). -
Call
logApplicationStartup(env)with the resultingConfigurableEnvironment.
Additionally:
-
@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 |
|
Protocol |
|
Server port |
|
Context path |
|
Local hostname |
|
Profile(s) |
|
Config Server |
|
Database |
|
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 |
|---|---|
|
Default. Liquibase auto-apply with faker data, local H2/MySQL, Hazelcast standalone, DEBUG logging for |
|
Liquibase with |
|
Spring Cloud Kubernetes config + discovery clients; Hazelcast K8s service-DNS discovery; plain-text logback (no ANSI) |
|
Enables |
|
OpenTelemetry spring-boot-starter + javaagent enabled, Logback OTLP appender (see OpenTelemetry Configuration) |
|
Configure |
|
Skip Liquibase on startup; used when the schema is managed out-of-band |
|
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 |
|---|---|
|
A permissive runtime built for feedback: verbose logging, relaxed CORS, stubs and mocks, faker data, auto-applied migrations, standalone caching. |
|
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) |
|
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 |
|---|---|
|
Free-form YAML merged into the rendered |
|
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 |
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
%crlfcustom converter (CRLFLogConverter) — strips CRLF characters from log messages to prevent log-injection exploits -
ANSI colours enabled under
devprofile; plain-text underkubernetes(so collectors like Loki don’t swallow the escape sequences) -
logback-appenderfor OTel trace correlation wired whenotlpprofile 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:
-
Container receives
SIGTERM. -
Spring initiates graceful shutdown — new connections rejected, in-flight requests given up to
spring.lifecycle.timeout-per-shutdown-phase(default 30s) to complete. -
Hazelcast member leaves the cluster; partition migration kicks in on remaining peers.
-
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 |
|---|---|
|
Main class; pre-context System property; profile validation; startup banner |
|
Portal variant of the same pattern |
|
Spring Cloud Kubernetes bootstrap |
|
Logging config with CRLF converter, OTel appender |
|
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 |