Spring BootExternalized Configuration
Spring Boot

Externalized Configuration

Externalized Configuration is Spring Boot's mechanism for separating configuration values from application code. Using @Value, @ConfigurationProperties, environment variables, command-line arguments, and Spring's property source hierarchy, you can run the same application binary in development, staging, and production without rebuilding — just swap the config.

Why Externalize Configuration

Externalized configuration is one of the Twelve-Factor App principles: config that varies between deployments (database URLs, API keys, feature flags) must live outside the code, not hardcoded or committed to version control. Spring Boot supports multiple configuration sources, evaluated in priority order: command-line arguments override environment variables, which override profile-specific application files, which override the base application.properties/yml. This layered system means you can set a sensible default in code and override it at any level without touching the JAR. The core goal: the same artifact (JAR/Docker image) runs identically in dev, staging, and production — only the externalized config changes.

Property Source Priority Order

Spring Boot evaluates configuration sources from highest to lowest priority. Higher-priority sources override lower ones for the same key.
Shell
# Priority order (1 = highest, overrides all below):

# 1. Command-line arguments
java -jar myapp.jar --server.port=9090 --spring.profiles.active=prod

# 2. @TestPropertySource (tests only)
# 3. @SpringBootTest properties attribute (tests only)

# 4. OS environment variables
export SPRING_DATASOURCE_URL=jdbc:mysql://prod-db:3306/mydb
export SERVER_PORT=8080

# 5. Java system properties (-D flags)
java -Dserver.port=8080 -Dspring.profiles.active=prod -jar myapp.jar

# 6. JNDI attributes (java:comp/env)

# 7. ServletContext / ServletConfig init params

# 8. Random value property source (random.*)
app.secret=${random.uuid}
app.number=${random.int}
app.bounded=${random.int[1, 100]}

# 9. Application properties outside the JAR
#    (file:./config/application.properties, file:./application.properties)

# 10. Application properties inside the JAR
#     (classpath:/config/application.properties, classpath:/application.properties)

# 11. @PropertySource annotations on @Configuration classes
@Configuration
@PropertySource("classpath:custom.properties")
public class AppConfig { }

# 12. Default properties (SpringApplication.setDefaultProperties)

# Practical implication:
# Set defaults in application.yml (priority 10).
# Override per-environment via env vars (priority 4) in Docker/K8s.
# Override for a single run via --flags (priority 1) during debugging.

@Value — Injecting Individual Properties

@Value is the simplest way to inject a single property into a bean. It uses Spring Expression Language (SpEL) syntax and supports default values, type conversion, and expressions.
Java
@Component
public class AppConfig {

    // Basic injection — fails with BeanCreationException if key is missing:
    @Value("${app.name}")
    private String appName;

    // With default value — uses "MyApp" if app.title is not set:
    @Value("${app.title:MyApp}")
    private String title;

    // Type conversion — Spring converts the string "8080" to int automatically:
    @Value("${server.port:8080}")
    private int serverPort;

    // Boolean with default:
    @Value("${feature.payments.enabled:false}")
    private boolean paymentsEnabled;

    // List injection — comma-separated value in properties:
    // app.allowed-origins=https://myapp.com,https://admin.myapp.com
    @Value("${app.allowed-origins}")
    private List<String> allowedOrigins;

    // SpEL expression — evaluate an expression at inject time:
    @Value("#{'${app.allowed-origins}'.split(',')}")
    private String[] originsArray;

    // Environment variable with fallback:
    @Value("${DATABASE_URL:${spring.datasource.url}}")
    private String dbUrl;

    // Random values (re-evaluated each context load):
    @Value("${random.uuid}")
    private String instanceId;
}

# application.yml:
app:
  name: MyApplication
  title: My Spring App
  allowed-origins: https://myapp.com,https://admin.myapp.com

# Limitations of @Value:
# - No validation (no @NotNull, @Min, etc.)
# - No IDE auto-completion for property names
# - Scattered across beans — hard to see all config in one place
# - No type-safe binding for nested structures
# Use @ConfigurationProperties for anything beyond simple scalar values.

@ConfigurationProperties — Type-Safe Binding

@ConfigurationProperties binds an entire prefix of properties to a Java class. This is the recommended approach for non-trivial configuration: it provides type safety, validation, IDE auto-completion (with the annotation processor), and a single place to see all related config.
Java
// 1. Define a @ConfigurationProperties class:
@ConfigurationProperties(prefix = "app")
@Validated  // enables Bean Validation on bound properties
public class AppProperties {

    @NotBlank
    private String name;

    private String title = "My App";    // default value

    @Min(1) @Max(65535)
    private int port = 8080;

    private List<String> allowedOrigins = new ArrayList<>();

    private final Jwt jwt = new Jwt();
    private final Mail mail = new Mail();
    private final Features features = new Features();

    // Nested class — bound to app.jwt.*:
    public static class Jwt {
        @NotBlank
        private String secret;
        private long expiration = 86400;
        private String issuer;
        // getters + setters
    }

    // Nested class with map — bound to app.mail.*:
    public static class Mail {
        private String from;
        private Map<String, String> templates = new HashMap<>();
        // getters + setters
    }

    // Nested class with boolean flags — bound to app.features.*:
    public static class Features {
        private boolean payments = false;
        private boolean notifications = true;
        // getters + setters
    }

    // getters + setters for all fields (or use records in Java 16+)
}

// 2. Register with @EnableConfigurationProperties or @ConfigurationPropertiesScan:
@SpringBootApplication
@ConfigurationPropertiesScan   // scans for all @ConfigurationProperties beans
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

// 3. Inject and use — just inject AppProperties anywhere:
@Service
@RequiredArgsConstructor
public class AuthService {

    private final AppProperties appProperties;

    public String generateToken(String subject) {
        return Jwts.builder()
            .setSubject(subject)
            .setIssuer(appProperties.getJwt().getIssuer())
            .setExpiration(new Date(System.currentTimeMillis()
                + appProperties.getJwt().getExpiration() * 1000))
            .signWith(Keys.hmacShaKeyFor(
                appProperties.getJwt().getSecret().getBytes()))
            .compact();
    }
}

Binding YAML to @ConfigurationProperties

The YAML that backs the AppProperties class above — showing how nested classes, lists, maps, and defaults map to YAML structure.
yaml
# application.yml — binds to AppProperties (prefix = "app"):

app:
  name: MyApplication
  title: My Spring App
  port: 8080

  allowed-origins:
    - https://myapp.com
    - https://admin.myapp.com
    - http://localhost:3000

  jwt:
    secret: ${JWT_SECRET}
    expiration: 86400
    issuer: https://myapp.com

  mail:
    from: noreply@myapp.com
    templates:
      welcome: classpath:templates/welcome.html
      reset: classpath:templates/password-reset.html

  features:
    payments: true
    notifications: true

# Spring Boot's relaxed binding rules:
# app.allowedOrigins = app.allowed-origins = APP_ALLOWED_ORIGINS (env var)
# All three bind to the same field — Spring normalizes them automatically.
# Convention: use kebab-case in YAML, UPPER_SNAKE_CASE for env vars.

# Overriding nested properties with environment variables (Docker/K8s):
# app.jwt.secret      → APP_JWT_SECRET
# app.mail.from       → APP_MAIL_FROM
# app.features.payments.enabled → APP_FEATURES_PAYMENTS_ENABLED

Environment Variables and Relaxed Binding

Spring Boot's relaxed binding maps environment variables to property keys automatically. This is the primary mechanism for injecting secrets and environment-specific config in Docker and Kubernetes deployments.
yaml
# Spring Boot relaxed binding — all four forms bind to the same property:
# spring.datasource.url         (YAML/properties canonical form)
# spring.datasource.url         (property key)
# spring.datasource-url         (not standard — avoid)
# SPRING_DATASOURCE_URL         (OS environment variable)
# spring.datasource.url         (Java system property)

# ── Docker — pass env vars to the container ───────────────────────────
docker run \
  -e SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/prod \
  -e SPRING_DATASOURCE_USERNAME=appuser \
  -e SPRING_DATASOURCE_PASSWORD=${DB_SECRET} \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e APP_JWT_SECRET=${JWT_SECRET} \
  myapp:latest

# ── Docker Compose ─────────────────────────────────────────────────────
services:
  app:
    image: myapp:latest
    environment:
      SPRING_PROFILES_ACTIVE: prod
      SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/mydb
      SPRING_DATASOURCE_USERNAME: appuser
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
      APP_JWT_SECRET: ${JWT_SECRET}
    env_file:
      - .env.prod    # load from file — keep secrets out of compose.yml

# ── Kubernetes — ConfigMap (non-sensitive config) ──────────────────────
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  SPRING_PROFILES_ACTIVE: "prod"
  SPRING_DATASOURCE_URL: "jdbc:mysql://db-service:3306/mydb"
  APP_MAIL_FROM: "noreply@myapp.com"

# ── Kubernetes — Secret (sensitive values) ─────────────────────────────
apiVersion: v1
kind: Secret
metadata:
  name: myapp-secrets
type: Opaque
stringData:
  SPRING_DATASOURCE_PASSWORD: "supersecret"
  APP_JWT_SECRET: "jwt-signing-key"

# Reference in deployment:
envFrom:
  - configMapRef:
      name: myapp-config
  - secretRef:
      name: myapp-secrets

Configuration Validation with @Validated

Adding @Validated to @ConfigurationProperties enables Bean Validation (JSR-380). Spring Boot validates all @ConfigurationProperties beans at startup — misconfiguration fails fast with a descriptive error rather than at runtime.
Java
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {

    @NotBlank(message = "app.name must not be blank")
    private String name;

    @Min(value = 1024, message = "app.port must be >= 1024")
    @Max(value = 65535, message = "app.port must be <= 65535")
    private int port;

    @Valid  // cascade validation into nested objects
    @NotNull
    private final Jwt jwt = new Jwt();

    @Valid
    @NotNull
    private final Database database = new Database();

    public static class Jwt {
        @NotBlank(message = "app.jwt.secret is required")
        @Size(min = 32, message = "app.jwt.secret must be at least 32 characters")
        private String secret;

        @Positive(message = "app.jwt.expiration must be positive")
        private long expiration = 86400;

        @NotBlank
        @Pattern(regexp = "https?://.+", message = "app.jwt.issuer must be a valid URL")
        private String issuer;
        // getters + setters
    }

    public static class Database {
        @NotBlank
        private String url;

        @Min(1) @Max(100)
        private int maxPoolSize = 10;
        // getters + setters
    }
}

// If validation fails at startup, Spring Boot throws:
// ***************************
// APPLICATION FAILED TO START
// ***************************
// Description:
//   Binding to target AppProperties failed:
//     Property: app.jwt.secret
//     Value: ""
//     Reason: app.jwt.secret is required
//
// This fails loudly at boot — not silently at 3am when the first request hits.

application.properties Metadata (IDE Auto-Completion)

Adding the Spring Boot Configuration Processor annotation processor generates additional-spring-configuration-metadata.json. This file powers IDE auto-completion, documentation, and type hints for your custom @ConfigurationProperties keys.
XML
<!-- pom.xml — add the annotation processor: -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

# After adding, rebuild and your custom properties get:
# - IDE auto-completion in application.yml / application.properties
# - Type information (int, boolean, List<String>)
# - Javadoc-sourced descriptions in the IDE tooltip
# - Deprecation warnings if you annotate with @DeprecatedConfigurationProperty

# You can also write manual hints in:
# src/main/resources/META-INF/additional-spring-configuration-metadata.json

{
  "properties": [
    {
      "name": "app.features.payments",
      "type": "java.lang.Boolean",
      "description": "Enable the payments feature module. Requires Stripe credentials.",
      "defaultValue": false
    },
    {
      "name": "app.jwt.expiration",
      "type": "java.lang.Long",
      "description": "JWT token expiration time in seconds.",
      "defaultValue": 86400
    }
  ]
}