Master Spring Profiles to manage environment-specific configurations (dev, staging, prod) and keep secrets safe. Learn core concepts and best practices
JOptimize Team
Spring Profiles is a mechanism to define environment-specific configurations (dev, staging, prod) and switch between them without changing code. Profiles allow you to activate different sets of properties, beans, and behaviors depending on where your application runs.
Instead of hardcoding database URLs, API keys, or feature flags, you create separate configuration files and let Spring load the right one based on the active profile. This keeps your codebase clean and deployments consistent.
Environment-specific configurations are essential in modern development because:
Without profiles, teams either commit secrets to Git (security nightmare) or manually edit config files before deployment (error-prone).
Spring Boot loads application-{profile}.yml automatically when a profile is active. Properties in profile files override the default application.yml.
File Structure:
application.yml # Default (all profiles) application-dev.yml # Dev environment application-staging.yml # Staging environment application-prod.yml # Production environment
Example:
application.yml (default):
server: port: 8080 logging: level: root: INFO
application-prod.yml (production override):
server: port: 8443 logging: level: root: WARN
When spring.profiles.active=prod, Spring merges both files—prod settings override defaults.
Spring resolves properties from multiple sources in strict order. Properties from higher-priority sources override those below them:
| Priority | Source | Example |
|----------|--------|---------|
| 1 (Highest) | Command-line arguments | --spring.datasource.url=jdbc:mysql://... |
| 2 | Environment variables | SPRING_DATASOURCE_URL=jdbc:mysql://... |
| 3 | Profile-specific files | application-{profile}.yml |
| 4 | Default application.yml | application.yml |
| 5 (Lowest) | Defaults in code | @Value("${key:default}") |
Example: How Precedence Works
When you run your app with different configurations:
# application.yml (base) app: db-url: jdbc:mysql://localhost:3306/myapp db-pool-size: 5 # application-prod.yml (profile-specific) app: db-url: jdbc:mysql://prod-db.company.com:3306/myapp # db-pool-size not overridden, inherits from application.yml
# If you run with environment variable: export APP_DB_POOL_SIZE=20 java -jar app.jar --spring.profiles.active=prod # Final resolved values: # - app.db-url = jdbc:mysql://prod-db.company.com:3306/myapp (from prod profile) # - app.db-pool-size = 20 (from ENV VAR, overrides both base and prod files)
Key Takeaway: Command-line args > Environment variables > Profile files > Base config > Code defaults
Conditionally register beans only when a specific profile is active. Use on @Configuration classes or @Bean methods to swap implementations per environment.
@Configuration @Profile("dev") public class DevConfig { @Bean public DataSource dataSource() { // In-memory H2 database for dev return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .build(); } } @Configuration @Profile("prod") public class ProdConfig { @Bean public DataSource dataSource() { // Production PostgreSQL with connection pooling HikariConfig config = new HikariConfig(); config.setJdbcUrl(env.getProperty("spring.datasource.url")); config.setUsername(env.getProperty("spring.datasource.username")); config.setPassword(env.getProperty("spring.datasource.password")); config.setMaximumPoolSize(20); return new HikariDataSource(config); } }
Binds external configuration to type-safe objects. Supports validation and nested properties across profiles.
@Configuration @ConfigurationProperties(prefix = "app.mail") public class MailProperties { private String from; private String host; private int port; // Getters/setters } // application-dev.yml app: mail: from: dev@localhost host: localhost port: 1025 // application-prod.yml app: mail: from: noreply@company.com host: smtp.sendgrid.net port: 587
Multiple ways to activate a profile:
# CLI argument (highest priority) java -jar app.jar --spring.profiles.active=prod # Environment variable export SPRING_PROFILES_ACTIVE=prod java -jar app.jar # application.yml default (fallback) spring: profiles: active: dev # IDE run configuration (IntelliJ, Eclipse) # VM options: -Dspring.profiles.active=dev
Use case: Production always uses env vars via Docker/K8s, dev uses IDE defaults, tests override programmatically.
Never store passwords or API keys in YAML files under version control. Use environment variables or external vaults.
# application-prod.yml (safe to commit—uses env vars) spring: datasource: url: ${DB_URL} username: ${DB_USERNAME} password: ${DB_PASSWORD} app: api-key: ${EXTERNAL_API_KEY}
Docker example:
FROM openjdk:17-slim COPY app.jar /app.jar ENV SPRING_PROFILES_ACTIVE=prod \ DB_URL=jdbc:postgresql://db:5432/prod \ DB_USERNAME=produser \ DB_PASSWORD=secretpassword ENTRYPOINT ["java", "-jar", "/app.jar"]
Always provide fallback defaults when a property might be missing.
@Component public class EmailService { @Value("${app.mail.from:noreply@example.com}") private String from; @Value("${app.mail.enabled:true}") private boolean enabled; public void send(String to, String subject, String body) { if (enabled) { // Send email using 'from' address } } }
If app.mail.from is missing, it defaults to noreply@example.com.
Combine multiple profiles under a single group name for cleaner multi-profile setups.
# application.yml spring: profiles: group: production: - prod - prod-security - prod-monitoring development: - dev - dev-debug
Usage:
java -jar app.jar --spring.profiles.active=production # Activates: prod, prod-security, prod-monitoring
Inspect the active profile at runtime to conditionally initialize beans.
@Component public class FeatureToggle { private final Environment env; public FeatureToggle(Environment env) { this.env = env; } public boolean isNewCheckoutEnabled() { return Arrays.asList(env.getActiveProfiles()) .contains("prod"); } }
src/main/resources/ ├── application.yml # Default (shared) ├── application-dev.yml # Dev environment ├── application-staging.yml # Staging └── application-prod.yml # Production
spring: application: name: joptimize-api jpa: hibernate: ddl-auto: validate jackson: serialization: write-dates-as-timestamps: false server: compression: enabled: true logging: level: root: INFO com.joptimize: DEBUG
spring: datasource: url: jdbc:h2:mem:testdb driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: create-drop show-sql: true h2: console: enabled: true server: port: 8080 logging: level: root: DEBUG org.hibernate.SQL: DEBUG org.hibernate.type.descriptor.sql.BasicBinder: TRACE app: mail: enabled: false
spring: datasource: url: ${DB_URL} username: ${DB_USERNAME} password: ${DB_PASSWORD} hikari: maximum-pool-size: 20 minimum-idle: 5 jpa: hibernate: ddl-auto: validate server: port: 8443 ssl: key-store: ${SSL_KEYSTORE_PATH} key-store-password: ${SSL_KEYSTORE_PASSWORD} logging: level: root: WARN com.joptimize: INFO app: mail: enabled: true host: ${SMTP_HOST} port: 587
# Development (uses application-dev.yml) ./mvnw spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=dev" # Production (uses application-prod.yml + env vars) java -jar target/joptimize-api.jar --spring.profiles.active=prod
| Problem | Solution |
|---------|----------|
| Secrets in version control | Use environment variables only, never hardcode |
| Profile not activating | Check spring.profiles.active spelling, check classpath |
| Properties not overriding | Remember precedence: CLI args > env vars > profile files > defaults |
| Wrong bean registered | Use @Profile consistently; test with @SpringBootTest(properties = ...) |
| Missing defaults cause NPE | Always use @Value("${key:default}") syntax |
Override profiles in unit tests to verify environment-specific behavior:
@SpringBootTest(properties = "spring.profiles.active=test") class MailServiceTest { @Test void testMailDisabledInTest() { // Uses application-test.yml or application.yml defaults } } @DataJpaTest @AutoConfigureTestDatabase(replace = Replace.ANY) class RepositoryTest { // Automatically uses in-memory H2 regardless of active profile }
Spring Profiles transform your deployment workflow:
✅ Centralize configuration per environment
✅ Keep secrets safe using environment variables
✅ Use type-safe beans with @ConfigurationProperties
✅ Conditionally load beans with @Profile and profile groups
✅ Test effectively by overriding profiles in tests
Master profiles, and your team will deploy faster, collaborate better, and keep secrets out of Git.
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.