Hardcoded config is a security incident waiting to happen. Learn Spring Boot profiles, environment variables, Spring Cloud Config, HashiCorp Vault integration, and config validation.
JOptimize Team
Configuration management is where many Spring Boot apps quietly accumulate security debt. Database passwords in application.properties committed to Git. JWT secrets hardcoded in configuration files. API keys that change in production but require a redeploy. These are not just inconveniences — they are security vulnerabilities.
Spring Boot resolves properties in this order (higher number = higher priority):
1. Default properties (SpringApplication.setDefaultProperties)
2. @PropertySource annotations
3. application.properties / application.yml
4. application-{profile}.properties
5. OS environment variables
6. Java system properties (-Dkey=value)
7. Command-line arguments (--key=value)
Environment variables (5) override application.properties (3) — this is the foundation of 12-factor app configuration.
src/main/resources/ application.properties # Shared across all environments application-dev.properties # Local development application-staging.properties # Staging environment application-prod.properties # Production
# application.properties — shared, safe to commit spring.application.name=order-service server.port=8080 spring.jpa.open-in-view=false # application-dev.properties — dev values spring.datasource.url=jdbc:postgresql://localhost:5432/orders_dev spring.jpa.hibernate.ddl-auto=create-drop logging.level.com.myapp=DEBUG # application-prod.properties — references env vars, no secrets spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USER} spring.datasource.password=${DATABASE_PASSWORD} logging.level.com.myapp=WARN
Activate profile:
# Via env var (recommended for containers) SPRING_PROFILES_ACTIVE=prod java -jar app.jar # Via system property java -Dspring.profiles.active=prod -jar app.jar
@ConfigurationProperties(prefix = "app.payment") @Validated public record PaymentConfig( @NotBlank String apiKey, @NotBlank String webhookSecret, @URL String callbackUrl, @Min(1) @Max(10) int maxRetries, @DurationUnit(ChronoUnit.SECONDS) Duration timeout ) {}
# application.properties app.payment.api-key=${PAYMENT_API_KEY} app.payment.webhook-secret=${PAYMENT_WEBHOOK_SECRET} app.payment.callback-url=https://myapp.com/webhooks/payment app.payment.max-retries=3 app.payment.timeout=30s
// Enable binding @SpringBootApplication @ConfigurationPropertiesScan public class Application { ... } // Use in service @Service @RequiredArgsConstructor public class PaymentService { private final PaymentConfig config; // Injected, validated at startup }
With @Validated, the app fails to start if PAYMENT_API_KEY is missing or blank — rather than failing at runtime when the first payment is attempted.
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-vault-config</artifactId> </dependency>
# bootstrap.properties (loaded before application.properties) spring.cloud.vault.uri=http://vault:8200 spring.cloud.vault.token=${VAULT_TOKEN} spring.cloud.vault.kv.enabled=true spring.cloud.vault.kv.default-context=order-service
# Store secrets in Vault vault kv put secret/order-service \ database.password=super-secret-db-pass \ jwt.secret=my-256-bit-secret
Spring Cloud Vault fetches secrets at startup and injects them as properties — no secret ever touches a config file or environment variable. Rotation is instant: update Vault, restart the app.
For Kubernetes without Vault, use mounted secrets:
# kubernetes deployment.yaml env: - name: DATABASE_PASSWORD valueFrom: secretKeyRef: name: db-secret key: password - name: JWT_SECRET valueFrom: secretKeyRef: name: jwt-secret key: secret
The Kubernetes secret is injected as an environment variable — Spring Boot picks it up via ${DATABASE_PASSWORD} in properties.
For centralized configuration across many microservices:
# Service A — bootstrap.properties spring.config.import=configserver:http://config-server:8888 spring.application.name=order-service
The Config Server serves properties from a Git repository, with per-service and per-profile overrides. Change a property, push to Git — all service instances refresh without restart (with Spring Cloud Bus + RabbitMQ/Kafka).
// DANGEROUS — secret in code, committed to Git @Value("eyJhbGciOiJIUzI1NiJ9.secret") private String jwtSecret; // SAFE — from environment variable @Value("${JWT_SECRET}") private String jwtSecret;
Add a pre-commit hook or CI step to detect hardcoded secrets:
# .pre-commit-config.yaml repos: - repo: https://github.com/Yelp/detect-secrets rev: v1.4.0 hooks: - id: detect-secrets
application-prod.properties with real values — prod config belongs in environment variables or a secrets manager, never in Gitspring.jpa.hibernate.ddl-auto=create-drop in prod — this drops and recreates your schema on startup; use validate in prod and Flyway for migrations@Validated on @ConfigurationProperties, missing secrets cause runtime failures instead of clean startup errors@ConfigurationProperties records in toString() will log all values including secrets; override toString() to mask sensitive fieldsProduction-grade Spring Boot configuration uses profiles to separate environments, externalized config via environment variables for secrets, @ConfigurationProperties with @Validated for type-safe early failure, and a secrets manager (Vault or Kubernetes Secrets) for credentials. The rule: if it's a secret, it never touches a config file committed to version control.
JOptimize flags hardcoded credentials in @Value annotations, ddl-auto=create-drop in non-dev profiles, and missing validation on @ConfigurationProperties classes.
Audit your configuration security before your credentials leak.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.