Back to Blog
spring-bootspring-profilesconfigurationbest-practices

How to Use Spring Profiles for Environment-Specific Configuration

Master Spring Profiles to manage environment-specific configurations (dev, staging, prod) and keep secrets safe. Learn core concepts and best practices

J

JOptimize Team

April 16, 2026· 5 min read

What It Is

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.


Why It Matters

Environment-specific configurations are essential in modern development because:

  • Deployment Flexibility: Deploy the same JAR to dev, staging, and production without rebuilding
  • Secret Management: Keep API keys, passwords, and credentials out of version control
  • Feature Toggling: Enable or disable features per environment without code changes
  • Resource Optimization: Use different database pools, cache settings, or log levels per environment
  • Team Collaboration: Developers, QA, and DevOps teams can work independently with their own profiles

Without profiles, teams either commit secrets to Git (security nightmare) or manually edit config files before deployment (error-prone).


Core Concepts

1. Profile-Specific Files

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.


2. Property Precedence

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


3. @Profile Annotation

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); } }

4. @ConfigurationProperties

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

Key Patterns

1. Activation Methods

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.


2. Externalize Secrets

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"]

3. Default Values with @Value

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.


4. Profile Groups (Spring Boot 2.4+)

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

5. Conditional Beans with Environment

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"); } }

Real-World Example: Multi-Profile Setup

Project Structure

src/main/resources/
├── application.yml                 # Default (shared)
├── application-dev.yml             # Dev environment
├── application-staging.yml         # Staging
└── application-prod.yml            # Production

application.yml (Default)

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

application-dev.yml

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

application-prod.yml

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

Running the Application

# 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

Common Pitfalls & Solutions

| 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 |


Testing with Profiles

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 }

Summary

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.


Resources

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.