Spring Boot Externalized Configuration: Properties, Profiles & Secrets Guide

Externalized configuration is the practice of keeping configuration outside the application binary so you can run the same JAR in development, staging, and production with different settings — no recompile needed. Spring Boot has one of the most flexible configuration systems in any framework, with 17 property sources evaluated in a specific priority order.

Property Source Priority Order

Spring Boot evaluates configuration sources in this order (highest to lowest priority):

  1. Command-line arguments (--server.port=9090)
  2. Java system properties (-Dserver.port=9090)
  3. OS environment variables (SERVER_PORT=9090)
  4. Profile-specific files (application-prod.yml)
  5. Application properties files (application.yml)
  6. Default properties (SpringApplication.setDefaultProperties())
Key rule: Higher in this list = wins. Environment variables override application.yml, which is why you can inject secrets via environment variables in Kubernetes without touching the deployed artifact.

application.yml vs application.properties

Both formats are fully supported. YAML is preferred for nested configuration because it is less repetitive:

# application.yml (preferred for complex config)
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: appuser
    password: ${DB_PASSWORD}  # reads from environment variable
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

server:
  port: 8080
  compression:
    enabled: true

app:
  jwt:
    secret: ${JWT_SECRET}
    expiration-ms: 86400000
  features:
    new-checkout: false

Note the ${DB_PASSWORD} syntax — Spring resolves this at startup from environment variables or other property sources.

@ConfigurationProperties — Type-Safe Binding

Avoid scattering @Value annotations everywhere. Group related properties into a typed class:

@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {

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

    public static class Jwt {
        @NotBlank
        private String secret;
        private long expirationMs = 86400000;
        // getters + setters
    }

    public static class Features {
        private boolean newCheckout = false;
        // getters + setters
    }

    public Jwt getJwt() { return jwt; }
    public Features getFeatures() { return features; }
}

Register it in your configuration class:

@SpringBootApplication
@EnableConfigurationProperties(AppProperties.class)
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

Inject and use it:

@Service
public class JwtService {
    private final AppProperties appProperties;

    public JwtService(AppProperties props) { this.appProperties = props; }

    public long getExpiry() {
        return appProperties.getJwt().getExpirationMs();
    }
}

Benefits of @ConfigurationProperties over @Value:

  • IDE autocompletion for property names
  • Validation with @Validated and JSR-303 annotations
  • Relaxed binding — app.jwt.expiration-ms, APP_JWT_EXPIRATION_MS, and app.jwt.expirationMs all bind to the same field

Spring Profiles

Profiles let you have different configuration per environment. Create profile-specific files:

src/main/resources/
├── application.yml          ← common config (all environments)
├── application-dev.yml      ← development overrides
├── application-staging.yml  ← staging overrides
└── application-prod.yml     ← production overrides

application-prod.yml example:

spring:
  datasource:
    url: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
  jpa:
    show-sql: false

logging:
  level:
    root: WARN
    com.mycompany: INFO

Activate a profile:

# Via environment variable (preferred in Docker/Kubernetes)
SPRING_PROFILES_ACTIVE=prod java -jar app.jar

# Via command line
java -jar app.jar --spring.profiles.active=prod

# Via application.yml (not recommended — defeats the purpose)
spring:
  profiles:
    active: dev

Environment Variables and Relaxed Binding

Spring Boot automatically converts environment variable names to property names. SPRING_DATASOURCE_URL maps to spring.datasource.url. This is the standard way to inject configuration into Docker containers and Kubernetes pods:

# Kubernetes Deployment
env:
  - name: SPRING_PROFILES_ACTIVE
    value: prod
  - name: DB_HOST
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: host
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: password

Spring Cloud Config Server

For managing configuration across many microservices, Spring Cloud Config Server centralises configuration in a Git repository:

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
# Config server application.yml
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/myorg/config-repo
          default-label: main

Each microservice fetches its configuration at startup from the config server by setting:

spring:
  config:
    import: "configserver:http://config-server:8888"
  application:
    name: order-service  # fetches order-service.yml from the repo

Secrets Management Best Practices

  • Never commit secrets to Git. Use ${ENV_VAR} placeholders in application.yml and inject values at runtime.
  • Use a secrets manager in production: AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault. Spring Cloud AWS and Spring Vault integrate directly with Spring Boot's property source system.
  • Encrypt values in config server: Spring Cloud Config supports {cipher}encryptedvalue syntax with a symmetric or asymmetric key.
  • Rotate credentials regularly and ensure your application handles config refresh via Spring Cloud Bus or a rolling restart strategy.
Local development tip: Use a .env file with dotenv-java to load secrets locally without committing them. Add .env to .gitignore immediately.

Validating Configuration at Startup

Fail fast rather than failing at runtime when configuration is missing or invalid. Add JSR-303 annotations to your @ConfigurationProperties class:

@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {

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

    @Min(value = 300000, message = "app.jwt.expiration-ms must be at least 5 minutes")
    private long jwtExpirationMs;

    @NotNull
    @Valid
    private DatabaseConfig database;

    // getters + setters
}

If validation fails, Spring Boot refuses to start and logs a clear error message — much better than a NullPointerException at 2am in production.

Conclusion

Spring Boot's externalized configuration system covers everything from simple application.yml files to centralized config servers and secrets managers. The key practices are: use @ConfigurationProperties over scattered @Value annotations, activate profiles via environment variables, never commit secrets to version control, and validate required configuration at startup so failures are caught immediately rather than at runtime.

Related: Spring Boot Hub | Spring Boot JWT Authentication