Adetayo Akinsanya unkletayo.dev

Spring Boot Externalized Configuration, Profiles, and Production Observability

Understanding PropertySources property resolution hierarchy, @ConfigurationProperties binding, and Actuator security

Part 19 in Series — Catch up on the previous article: Dynamic Proxies in Spring: JDK Dynamic Proxies vs CGLIB Bytecode Generation (Part 18) before diving into this post.

Why You Need This in Real Life

During a midnight production incident, a DevOps team hits https://api.payments.com/actuator/env to diagnose a sudden microservice CPU spike. To their horror, the public /actuator/env endpoint exposes plain-text production database passwords, AWS secret access keys, and internal API keys to the open internet (OWASP Category A05: Security Misconfiguration / Exposed Sensitive Data).

To run secure microservices in cloud environments, you must master Spring’s PropertySources resolution hierarchy, @ConfigurationProperties binding mechanics, and Spring Boot Actuator security controls.


Part 1: Spring’s PropertySources Precedence Hierarchy

Spring Boot loads configuration properties from multiple sources into a unified ConfigurableEnvironment abstraction containing a MutablePropertySources chain.

When a property value is requested (env.getProperty("server.port")), Spring iterates through property sources in strict precedence order. First match wins!

                       High Precedence (Overrides Everything Below)
                                       |
  1. Command Line Arguments (--server.port=9090)
        |
  2. SPRING_APPLICATION_JSON (Inline JSON string)
        |
  3. ServletConfig / ServletContext Parameters
        |
  4. System Properties (System.getProperties() / -Dserver.port=8080)
        |
  5. OS Environment Variables (export SERVER_PORT=7070)
        |
  6. Profile-Specific Properties (application-prod.properties)
        |
  7. Application Properties (application.properties / application.yml)
        |
  8. Default Properties (SpringApplication.setDefaultProperties)
        |
                        Low Precedence (Fallback Defaults)

The OS Environment Variable Name Rule (Relaxed Binding)

Operating systems like Linux prohibit period characters (.) and hyphens (-) in environment variable names. Spring Boot automatically translates environment variables using relaxed binding rules:

  • spring.datasource.url \rightarrow SPRING_DATASOURCE_URL
  • app.payment-v2.timeout-ms \rightarrow APP_PAYMENTV2_TIMEOUTMS or APP_PAYMENT_V2_TIMEOUT_MS

Part 2: @Value vs @ConfigurationProperties

Spring provides two mechanisms for injecting configuration properties into Java beans:

1. @Value("${property.name}") (Ad-hoc Field Injection)

Injects individual values directly into bean fields:

@Component
public class CurrencyService {

    @Value("${currency.default-code:USD}") // Fallback default "USD" if missing
    private String defaultCode;

    @Value("${currency.max-limit}")
    private BigDecimal maxLimit;
}
  • Drawbacks: No type-safe group validation, no hierarchical prefix mapping, fragile with complex maps or nested lists.

2. @ConfigurationProperties (Type-Safe Strongly Typed Beans)

Binds entire property trees to structured Java record or POJO classes with compile-time validation:

package com.example.config;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Validated
@ConfigurationProperties(prefix = "app.payment")
public record PaymentProperties(
        @NotBlank String providerUrl,
        @Min(100) @Max(10000) int timeoutMs,
        RetryProperties retry
) {
    public record RetryProperties(int maxAttempts, long backoffMs) {}
}
# application.properties
app.payment.provider-url=https://api.stripe.com
app.payment.timeout-ms=5000
app.payment.retry.max-attempts=3
app.payment.retry.backoff-ms=1000

Part 3: Production Observability with Spring Boot Actuator

Spring Boot Actuator adds HTTP endpoints to monitor and manage application operational metrics, health status, thread dumps, and heap logs in production.

Essential Production Actuator Endpoints

EndpointPathOperational Utility
Health/actuator/healthShows application, database, Disk, and Redis health status. Used by Kubernetes Liveness/Readiness probes.
Metrics/actuator/metricsExposes Micrometer metrics (JVM heap memory, GC pauses, Tomcat thread count, HTTP request latency).
Prometheus/actuator/prometheusFormats metrics into Prometheus scrape format.
Thread Dump/actuator/threaddumpDumps JVM thread stack traces to diagnose CPU spikes or thread deadlocks.
Heap Dump/actuator/heapdumpDownloads a binary HPROF heap dump file for memory leak analysis.

Part 4: Kubernetes Probes & Actuator Security Hardening

1. Configuring Kubernetes Liveness and Readiness Probes

Kubernetes relies on HTTP probes to manage container lifecycles:

  • Liveness Probe (/actuator/health/liveness): Checks if the JVM process is alive. If it fails, Kubernetes restarts the pod container.
  • Readiness Probe (/actuator/health/readiness): Checks if the application can accept incoming traffic (e.g., database connection pool is warm). If it fails, Kubernetes removes the pod from service load balancers.

Enable dedicated probe endpoints in application.properties:

management.endpoint.health.probes.enabled=true
management.health.livenessstate.enabled=true
management.health.readinessstate.enabled=true

2. Hardening Actuator Endpoints Against Data Exposure

By default, Spring Boot 3 hides sensitive endpoints. Never expose * management endpoints publicly!

# SECURE CONFIGURATION: Only expose health and prometheus metrics publicly
management.endpoints.web.exposure.include=health,prometheus

# Hide detailed health error details from unauthenticated users
management.endpoint.health.show-details=when_authorized

# Separate management port (e.g., app runs on 8080, actuator metrics on internal management port 8081)
management.server.port=8081

By running Actuator endpoints on an isolated internal port (8081), you can block port 8081 at your cloud API gateway while allowing internal Prometheus servers to scrape metrics safely inside your VPC network.


Next Steps

We are now ready for the final culminating post of the series: Module 7 Capstone Project. We will build a complete, runnable Mini Spring Boot Framework in Java from scratch, incorporating IoC, reflection component scanning, custom auto-configuration, dynamic transaction proxies, and an embedded HTTP web server!

References & Further Reading

  1. Reactive Streams JVM Standard. Reactive Streams Specification v1.0.4. Reactive Streams Docs.
  2. Project Reactor. Project Reactor Core Architecture (Mono and Flux). Reactor Docs.
  3. Spring.io. Spring WebFlux Reference Manual — Reactive Web Applications with Spring. Spring Docs.

Up Next in Series →

Part 20: Building a Custom Mini Spring Boot Framework in Java: The Spring Capstone

Continue to Part 20 →